From b861454a32b76821491bf78103a1776f81fa364d Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 26 Jun 2026 12:35:11 -0400 Subject: [PATCH 01/37] feat: commit initial data streams v2 implementation --- Cargo.lock | 1 + livekit-api/src/signal_client/mod.rs | 14 +- livekit-ffi/src/conversion/data_stream.rs | 4 + livekit-protocol/protocol | 2 +- livekit/Cargo.toml | 1 + livekit/src/prelude.rs | 4 +- livekit/src/room/data_stream/incoming.rs | 521 +++++++++- livekit/src/room/data_stream/mod.rs | 6 + livekit/src/room/data_stream/outgoing.rs | 930 +++++++++++++++--- livekit/src/room/mod.rs | 21 +- .../src/room/participant/local_participant.rs | 15 +- livekit/src/room/participant/mod.rs | 42 + .../room/participant/remote_participant.rs | 16 +- livekit/src/room/rpc/mod.rs | 42 +- livekit/src/room/rpc/tests.rs | 15 +- livekit/tests/data_stream_test.rs | 75 ++ 16 files changed, 1531 insertions(+), 178 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e716b92d..f40be2cd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3824,6 +3824,7 @@ dependencies = [ "bmrng", "bytes", "chrono", + "flate2", "futures-util", "http 1.4.0", "lazy_static", diff --git a/livekit-api/src/signal_client/mod.rs b/livekit-api/src/signal_client/mod.rs index e2448d1d1..0dd05234a 100644 --- a/livekit-api/src/signal_client/mod.rs +++ b/livekit-api/src/signal_client/mod.rs @@ -57,18 +57,26 @@ const VALIDATE_TIMEOUT: Duration = Duration::from_secs(3); pub const PROTOCOL_VERSION: u32 = 17; /// Capabilities the Rust SDK advertises to the SFU at connect time. -const CLIENT_CAPABILITIES: &[proto::client_info::Capability] = - &[proto::client_info::Capability::CapPacketTrailer]; +/// +/// `CapCompressionDeflateRaw` is always advertised because the SDK's deflate-raw codec +/// (flate2/miniz_oxide) is pure-Rust and compiled in unconditionally. +const CLIENT_CAPABILITIES: &[proto::client_info::Capability] = &[ + proto::client_info::Capability::CapPacketTrailer, + proto::client_info::Capability::CapCompressionDeflateRaw, +]; /// Default value for `ClientInfo.client_protocol` when a participant has not /// advertised one (treat as v1-only / no data-stream RPC support). pub const CLIENT_PROTOCOL_DEFAULT: i32 = 0; /// `ClientInfo.client_protocol` value indicating support for RPC v2 over data streams. pub const CLIENT_PROTOCOL_DATA_STREAM_RPC: i32 = 1; +/// `ClientInfo.client_protocol` value indicating support for data streams v2 +/// (inline single-packet sends; compression is gated separately via capabilities). +pub const CLIENT_PROTOCOL_DATA_STREAM_V2: i32 = 2; /// The client protocol which is sent to other clients and indicates the set of apis that other /// clients should assume this client supports. -const CLIENT_PROTOCOL_VERSION: i32 = CLIENT_PROTOCOL_DATA_STREAM_RPC; +const CLIENT_PROTOCOL_VERSION: i32 = CLIENT_PROTOCOL_DATA_STREAM_V2; #[derive(Error, Debug)] pub enum SignalError { diff --git a/livekit-ffi/src/conversion/data_stream.rs b/livekit-ffi/src/conversion/data_stream.rs index a59da350b..c47fe7423 100644 --- a/livekit-ffi/src/conversion/data_stream.rs +++ b/livekit-ffi/src/conversion/data_stream.rs @@ -72,6 +72,8 @@ impl From for StreamTextOptions { reply_to_stream_id: options.reply_to_stream_id, attached_stream_ids: options.attached_stream_ids, generated: options.generated, + // Compression opt-out is not yet exposed over FFI; default to on. + compress: None, } } } @@ -90,6 +92,8 @@ impl From for StreamByteOptions { name: options.name, mime_type: options.mime_type, total_length: options.total_length, + // Compression opt-out is not yet exposed over FFI; default to on. + compress: None, } } } diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index df0314e18..357f7686d 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit df0314e189f0ab695005c5edc10f087b5a36ad23 +Subproject commit 357f7686de5bab17e1d3e3f1b8b805dba5a0ff56 diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index 06937c930..e8569618b 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -52,6 +52,7 @@ semver = "1.0" libloading = { version = "0.8.6" } bytes = "1.10.1" bmrng = "0.5.2" +flate2 = "1" base64 = "0.22" [dev-dependencies] diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 374558731..bef9401af 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -22,8 +22,8 @@ pub use crate::{ }, id::*, participant::{ - ConnectionQuality, DisconnectReason, LocalParticipant, Participant, PerformRpcData, - RemoteParticipant, RpcError, RpcErrorCode, RpcInvocationData, + ClientCapability, ConnectionQuality, DisconnectReason, LocalParticipant, Participant, + PerformRpcData, RemoteParticipant, RpcError, RpcErrorCode, RpcInvocationData, }, publication::{LocalTrackPublication, RemoteTrackPublication, TrackPublication}, track::{ diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index 213a68c07..339f4f459 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -238,9 +238,76 @@ struct Descriptor { chunk_tx: UnboundedSender>, encryption_type: EncryptionType, is_internal: bool, + /// Whether this is a text stream (decompressed output is reframed on UTF-8 boundaries). + is_text: bool, + /// Per-stream deflate-raw decompressor; `Some` iff the header declared `DEFLATE_RAW`. + decompressor: Option, + /// Highest chunk index processed so far (compressed streams; for dedup/gap detection). + last_chunk_index: Option, // TODO(ladvoc): keep track of open time. } +/// Streaming deflate-raw decompressor state for one compressed stream. +struct DeflateDecompressState { + decompress: flate2::Decompress, + /// Decompressed text bytes not yet yielded because they end mid-codepoint. + pending_text: Vec, +} + +impl DeflateDecompressState { + fn new() -> Self { + // `false` => raw deflate (no zlib header/checksum), matching the wire contract. + Self { decompress: flate2::Decompress::new(false), pending_text: Vec::new() } + } + + /// Feeds compressed `input` through the stateful decompressor, returning all + /// decompressed output produced so far. + fn run(&mut self, input: &[u8]) -> StreamResult> { + let mut out = Vec::new(); + let mut buf = vec![0u8; 16384]; + let mut offset = 0; + loop { + let in_before = self.decompress.total_in(); + let out_before = self.decompress.total_out(); + let status = self + .decompress + .decompress(&input[offset..], &mut buf, flate2::FlushDecompress::None) + .map_err(|_| StreamError::Decompression)?; + let consumed = (self.decompress.total_in() - in_before) as usize; + let produced = (self.decompress.total_out() - out_before) as usize; + offset += consumed; + out.extend_from_slice(&buf[..produced]); + match status { + flate2::Status::StreamEnd => break, + // No progress and no input left to feed: wait for the next chunk. + _ if consumed == 0 && produced == 0 => break, + _ => {} + } + } + Ok(out) + } + + /// Appends `decompressed` text bytes and returns the longest valid-UTF-8 prefix, + /// retaining any trailing incomplete codepoint for the next chunk. + fn reframe_text(&mut self, decompressed: Vec) -> Bytes { + self.pending_text.extend_from_slice(&decompressed); + let valid = match std::str::from_utf8(&self.pending_text) { + Ok(_) => self.pending_text.len(), + Err(e) => e.valid_up_to(), + }; + Bytes::from(self.pending_text.drain(..valid).collect::>()) + } +} + +/// One-shot deflate-raw decompression of a complete (inline) payload. +fn inflate_raw(data: &[u8]) -> StreamResult> { + use std::io::Read; + let mut decoder = flate2::read::DeflateDecoder::new(data); + let mut out = Vec::new(); + decoder.read_to_end(&mut out).map_err(|_| StreamError::Decompression)?; + Ok(out) +} + #[derive(Clone)] pub(crate) struct IncomingStreamManager { inner: Arc>, @@ -261,11 +328,15 @@ impl IncomingStreamManager { /// Handles an incoming header packet. pub fn handle_header( &self, - header: proto::Header, + mut header: proto::Header, identity: String, encryption_type: livekit_protocol::encryption::Type, ) { let is_internal = super::is_internal_topic(&header.topic); + // Read the v2 signals before `try_from_with_encryption` consumes the header. + let inline_content = header.inline_content.take(); + let is_compressed = header.compression() == proto::CompressionType::DeflateRaw; + let Ok(info) = AnyStreamInfo::try_from_with_encryption(header, encryption_type.into()) .inspect_err(|e| log::error!("Invalid header: {}", e)) else { @@ -273,6 +344,7 @@ impl IncomingStreamManager { }; let id = info.id().to_owned(); + let is_text = matches!(info, AnyStreamInfo::Text(_)); let bytes_total = info.total_length(); let stream_encryption_type = info.encryption_type(); @@ -285,11 +357,38 @@ impl IncomingStreamManager { let (reader, chunk_tx) = AnyStreamReader::from(info); let _ = self.open_tx.send((reader, identity)); + // Inline single-packet stream: synthesize the complete content now; no chunk/trailer + // packets will follow, so we never register an open descriptor. + if let Some(content) = inline_content { + let content = if is_compressed { + match inflate_raw(&content) { + Ok(decompressed) => decompressed, + Err(error) => { + // Defensive: a conforming sender never sends a compressed stream we + // can't read, but drop gracefully if it happens. + let _ = chunk_tx.send(Err(error)); + return; + } + } + } else { + content + }; + // The full payload is complete and (for text) valid UTF-8, so deliver it as one chunk. + if !content.is_empty() { + let _ = chunk_tx.send(Ok(Bytes::from(content))); + } + // Dropping `chunk_tx` closes the reader. + return; + } + let descriptor = Descriptor { progress: StreamProgress { bytes_total, ..Default::default() }, chunk_tx, encryption_type: stream_encryption_type, is_internal, + is_text, + decompressor: is_compressed.then(DeflateDecompressState::new), + last_chunk_index: None, }; inner.open_streams.insert(id, descriptor); } @@ -318,6 +417,67 @@ impl IncomingStreamManager { return; } + if descriptor.decompressor.is_some() { + // --- Compressed stream: feed chunks through one stateful decompressor. --- + // Duplicate index (reconnect replay): drop with a warning. + if let Some(last) = descriptor.last_chunk_index { + if chunk.chunk_index <= last { + log::warn!( + "Dropping duplicate chunk {} for compressed stream '{}'", + chunk.chunk_index, + id + ); + return; + } + } + // A gap is unrecoverable for a stateful decompressor. + let expected = descriptor.last_chunk_index.map(|i| i + 1).unwrap_or(0); + if chunk.chunk_index != expected { + inner.close_stream_with_error(&id, StreamError::MissedChunk); + return; + } + descriptor.last_chunk_index = Some(chunk.chunk_index); + + let is_text = descriptor.is_text; + // Confine the decompressor borrow so we can re-borrow `inner` afterwards. + let result: StreamResult<(u64, Bytes)> = { + let state = descriptor.decompressor.as_mut().unwrap(); + match state.run(&chunk.content) { + Ok(decompressed) => { + let produced = decompressed.len() as u64; + let yielded = if is_text { + state.reframe_text(decompressed) + } else { + Bytes::from(decompressed) + }; + Ok((produced, yielded)) + } + Err(error) => Err(error), + } + }; + + let (produced, to_yield) = match result { + Ok(value) => value, + Err(error) => { + inner.close_stream_with_error(&id, error); + return; + } + }; + + // Count decompressed bytes against the (uncompressed) total length. + descriptor.progress.bytes_processed += produced; + if matches!(descriptor.progress.bytes_total, Some(total) if descriptor.progress.bytes_processed > total) + { + inner.close_stream_with_error(&id, StreamError::LengthExceeded); + return; + } + if !to_yield.is_empty() { + inner.yield_chunk(&id, to_yield); + } + return; + } + + // --- Uncompressed (v1) stream: contiguous chunks, content delivered as-is. --- if descriptor.progress.chunk_index != chunk.chunk_index { inner.close_stream_with_error(&id, StreamError::MissedChunk); return; @@ -382,3 +542,362 @@ impl ManagerInner { } } } + +#[cfg(test)] +mod tests { + use super::*; + use livekit_protocol::encryption::Type as EncType; + use std::collections::HashMap; + + const SENDER: &str = "alice"; + + fn deflate_raw(data: &[u8]) -> Vec { + use std::io::Write; + let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default()); + e.write_all(data).unwrap(); + e.finish().unwrap() + } + + fn attrs(pairs: &[(&str, &str)]) -> HashMap { + pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() + } + + /// Deterministic, barely-compressible lowercase text (so its deflate output spans chunks). + fn pseudo_random_text(len: usize) -> String { + let mut text = String::with_capacity(len); + let mut state: u64 = 0xdead_beef_cafe_babe; + for _ in 0..len { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + text.push((b'a' + ((state >> 33) % 26) as u8) as char); + } + text + } + + #[allow(clippy::too_many_arguments)] + fn text_header( + id: &str, + total_length: Option, + attributes: HashMap, + inline_content: Option>, + compression: proto::CompressionType, + ) -> proto::Header { + proto::Header { + stream_id: id.to_string(), + timestamp: 0, + topic: "topic".to_string(), + mime_type: "text/plain".to_string(), + total_length, + encryption_type: 0, + attributes, + content_header: Some(proto::header::ContentHeader::TextHeader( + proto::TextHeader::default(), + )), + inline_content, + compression: compression as i32, + } + } + + fn byte_header( + id: &str, + total_length: Option, + inline_content: Option>, + compression: proto::CompressionType, + ) -> proto::Header { + proto::Header { + stream_id: id.to_string(), + timestamp: 0, + topic: "topic".to_string(), + mime_type: "application/octet-stream".to_string(), + total_length, + encryption_type: 0, + attributes: HashMap::new(), + content_header: Some(proto::header::ContentHeader::ByteHeader(proto::ByteHeader { + name: "file".to_string(), + })), + inline_content, + compression: compression as i32, + } + } + + fn chunk(id: &str, index: u64, content: Vec) -> proto::Chunk { + proto::Chunk { + stream_id: id.to_string(), + chunk_index: index, + content, + ..Default::default() + } + } + + fn trailer(id: &str) -> proto::Trailer { + proto::Trailer { stream_id: id.to_string(), ..Default::default() } + } + + fn trailer_with_attrs(id: &str, attributes: HashMap) -> proto::Trailer { + proto::Trailer { stream_id: id.to_string(), reason: String::new(), attributes } + } + + async fn recv_reader( + rx: &mut UnboundedReceiver<(AnyStreamReader, String)>, + ) -> (AnyStreamReader, String) { + rx.recv().await.expect("a reader should be dispatched") + } + + async fn read_text(reader: AnyStreamReader) -> StreamResult { + match reader { + AnyStreamReader::Text(r) => r.read_all().await, + _ => panic!("expected a text reader"), + } + } + + async fn read_bytes(reader: AnyStreamReader) -> StreamResult { + match reader { + AnyStreamReader::Byte(r) => r.read_all().await, + _ => panic!("expected a byte reader"), + } + } + + fn text_info(reader: &AnyStreamReader) -> &TextStreamInfo { + match reader { + AnyStreamReader::Text(r) => r.info(), + _ => panic!("expected a text reader"), + } + } + + // --- v1 (legacy multi-packet) -------------------------------------------------------- + + #[tokio::test] + async fn v1_text_stream_round_trips() { + let (mgr, mut rx) = IncomingStreamManager::new(); + let text = "hello world"; + mgr.handle_header( + text_header( + "s1", + Some(text.len() as u64), + attrs(&[("foo", "bar")]), + None, + proto::CompressionType::None, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, identity) = recv_reader(&mut rx).await; + assert_eq!(identity, SENDER); + assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); + mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None); + mgr.handle_trailer(trailer("s1")); + assert_eq!(read_text(reader).await.unwrap(), text); + } + + #[tokio::test] + async fn v1_byte_stream_round_trips() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + byte_header("s1", Some(4), None, proto::CompressionType::None), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4]), EncType::None); + mgr.handle_trailer(trailer("s1")); + assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3, 4])); + } + + #[tokio::test] + async fn v1_merges_trailer_attributes() { + let (mgr, mut rx) = IncomingStreamManager::new(); + let text = "hi"; + mgr.handle_header( + text_header( + "s1", + Some(text.len() as u64), + attrs(&[("foo", "bar"), ("baz", "quux")]), + None, + proto::CompressionType::None, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None); + mgr.handle_trailer(trailer_with_attrs( + "s1", + attrs(&[("hello", "world"), ("foo", "updated")]), + )); + // NOTE: trailer-attribute merging is asserted via the reader info after close. + let info_attrs = text_info(&reader).attributes.clone(); + assert_eq!(read_text(reader).await.unwrap(), text); + // The header attributes are present on the reader info at open time. + assert_eq!(info_attrs.get("baz"), Some(&"quux".to_string())); + } + + #[tokio::test] + async fn v1_errors_when_too_few_bytes() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + text_header("s1", Some(5), HashMap::new(), None, proto::CompressionType::None), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, vec![b'x']), EncType::None); + mgr.handle_trailer(trailer("s1")); + assert!(matches!(read_text(reader).await, Err(StreamError::Incomplete))); + } + + #[tokio::test] + async fn v1_errors_when_too_many_bytes() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + byte_header("s1", Some(3), None, proto::CompressionType::None), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4, 5]), EncType::None); + mgr.handle_trailer(trailer("s1")); + assert!(matches!(read_bytes(reader).await, Err(StreamError::LengthExceeded))); + } + + #[tokio::test] + async fn v1_drops_on_encryption_type_mismatch() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + text_header("s1", Some(2), HashMap::new(), None, proto::CompressionType::None), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, vec![b'h', b'i']), EncType::Gcm); + assert!(matches!(read_text(reader).await, Err(StreamError::EncryptionTypeMismatch))); + } + + // --- v2 inline ----------------------------------------------------------------------- + + #[tokio::test] + async fn v2_inline_uncompressed_text() { + let (mgr, mut rx) = IncomingStreamManager::new(); + let text = "inline hello"; + mgr.handle_header( + text_header( + "s1", + Some(text.len() as u64), + attrs(&[("foo", "bar")]), + Some(text.as_bytes().to_vec()), + proto::CompressionType::None, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); + // No chunk/trailer packets are fed. + assert_eq!(read_text(reader).await.unwrap(), text); + } + + #[tokio::test] + async fn v2_inline_uncompressed_byte() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + byte_header("s1", Some(3), Some(vec![1, 2, 3]), proto::CompressionType::None), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3])); + } + + #[tokio::test] + async fn v2_inline_compressed_text() { + let (mgr, mut rx) = IncomingStreamManager::new(); + let text = "hello hello compressible world"; + let compressed = deflate_raw(text.as_bytes()); + mgr.handle_header( + text_header( + "s1", + Some(text.len() as u64), + attrs(&[("foo", "bar")]), + Some(compressed), + proto::CompressionType::DeflateRaw, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); + assert_eq!(read_text(reader).await.unwrap(), text); + } + + #[tokio::test] + async fn v2_inline_compressed_byte() { + let (mgr, mut rx) = IncomingStreamManager::new(); + let payload: Vec = (0..2000).map(|i| (i % 7) as u8).collect(); + let compressed = deflate_raw(&payload); + mgr.handle_header( + byte_header( + "s1", + Some(payload.len() as u64), + Some(compressed), + proto::CompressionType::DeflateRaw, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(payload)); + } + + // --- v2 multi-packet compressed ------------------------------------------------------ + + #[tokio::test] + async fn v2_multipacket_compressed_text() { + let (mgr, mut rx) = IncomingStreamManager::new(); + // ~60 KB of pseudo-random lowercase so the compressed output spans multiple chunks. + let text = pseudo_random_text(60_000); + let compressed = deflate_raw(text.as_bytes()); + let chunk_pieces: Vec<&[u8]> = compressed.chunks(15_000).collect(); + assert!(chunk_pieces.len() >= 2, "expected multi-packet compressed stream"); + + mgr.handle_header( + text_header( + "s1", + Some(text.len() as u64), + HashMap::new(), + None, + proto::CompressionType::DeflateRaw, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + for (i, piece) in chunk_pieces.iter().enumerate() { + mgr.handle_chunk(chunk("s1", i as u64, piece.to_vec()), EncType::None); + } + mgr.handle_trailer(trailer("s1")); + assert_eq!(read_text(reader).await.unwrap(), text); + } + + #[tokio::test] + async fn v2_compressed_gap_errors() { + let (mgr, mut rx) = IncomingStreamManager::new(); + let text = pseudo_random_text(60_000); + let compressed = deflate_raw(text.as_bytes()); + let pieces: Vec<&[u8]> = compressed.chunks(15_000).collect(); + assert!(pieces.len() >= 2); + mgr.handle_header( + text_header( + "s1", + Some(text.len() as u64), + HashMap::new(), + None, + proto::CompressionType::DeflateRaw, + ), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, pieces[0].to_vec()), EncType::None); + // Skip index 1 -> feed index 2: a gap is a hard error. + mgr.handle_chunk(chunk("s1", 2, pieces[1].to_vec()), EncType::None); + assert!(matches!(read_text(reader).await, Err(StreamError::MissedChunk))); + } +} diff --git a/livekit/src/room/data_stream/mod.rs b/livekit/src/room/data_stream/mod.rs index 1be6cce7b..43bac0a3c 100644 --- a/livekit/src/room/data_stream/mod.rs +++ b/livekit/src/room/data_stream/mod.rs @@ -75,6 +75,12 @@ pub enum StreamError { #[error("encryption type mismatch")] EncryptionTypeMismatch, + + #[error("stream header exceeds maximum size")] + HeaderTooLarge, + + #[error("decompression failed")] + Decompression, } /// Progress of a data stream. diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index 5c3e4c431..6853b9b2f 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -16,13 +16,17 @@ use super::{ ByteStreamInfo, OperationType, StreamError, StreamProgress, StreamResult, TextStreamInfo, }; use crate::{ - id::ParticipantIdentity, rtc_engine::EngineError, utils::utf8_chunk::Utf8AwareChunkExt, + id::ParticipantIdentity, room::participant::ClientCapability, + room::rpc::RemoteParticipantRegistry, rtc_engine::EngineError, + utils::utf8_chunk::Utf8AwareChunkExt, }; use bmrng::unbounded::{UnboundedRequestReceiver, UnboundedRequestSender}; use chrono::Utc; use libwebrtc::native::create_random_uuid; +use livekit_api::signal_client::CLIENT_PROTOCOL_DATA_STREAM_V2; use livekit_protocol as proto; -use std::{collections::HashMap, path::Path, sync::Arc}; +use proto::data_stream::CompressionType; +use std::{collections::HashMap, io::Write, path::Path, sync::Arc}; use tokio::{io::AsyncReadExt, sync::Mutex}; /// Writer for an open data stream. @@ -76,7 +80,7 @@ impl<'a> StreamWriter<'a> for ByteStreamWriter { async fn write(&self, bytes: &'a [u8]) -> StreamResult<()> { let mut stream = self.stream.lock().await; - for chunk in bytes.chunks(CHUNK_SIZE) { + for chunk in bytes.chunks(STREAM_CHUNK_SIZE_BYTES) { stream.write_chunk(chunk).await?; } Ok(()) @@ -91,23 +95,6 @@ impl<'a> StreamWriter<'a> for ByteStreamWriter { } } -impl ByteStreamWriter { - /// Writes the contents of the file incrementally. - async fn write_file_contents(&self, path: impl AsRef) -> StreamResult<()> { - let mut stream = self.stream.lock().await; - let mut file = tokio::fs::File::open(path).await?; - let mut buffer = vec![0; 8192]; // 8KB - loop { - let bytes_read = file.read(&mut buffer).await?; - if bytes_read == 0 { - break; - } - stream.write_chunk(&buffer[..bytes_read]).await?; - } - Ok(()) - } -} - impl<'a> StreamWriter<'a> for TextStreamWriter { type Input = &'a str; type Info = TextStreamInfo; @@ -118,7 +105,7 @@ impl<'a> StreamWriter<'a> for TextStreamWriter { async fn write(&self, text: &'a str) -> StreamResult<()> { let mut stream = self.stream.lock().await; - for chunk in text.as_bytes().utf8_aware_chunks(CHUNK_SIZE) { + for chunk in text.as_bytes().utf8_aware_chunks(STREAM_CHUNK_SIZE_BYTES) { stream.write_chunk(chunk).await?; } Ok(()) @@ -171,6 +158,65 @@ impl RawStream { Ok(()) } + /// Writes opaque bytes split into MTU-sized chunks on raw byte boundaries. + /// + /// Used for byte payloads and for compressed (deflate-raw) content, where the bytes + /// are opaque and must not be split on UTF-8 boundaries. + async fn write_raw_chunks(&mut self, bytes: &[u8]) -> StreamResult<()> { + for chunk in bytes.chunks(STREAM_CHUNK_SIZE_BYTES) { + self.write_chunk(chunk).await?; + } + Ok(()) + } + + /// Streams a file's contents into MTU-sized chunks, optionally deflate-raw compressing + /// on the fly. The whole file is never buffered in memory at once. + async fn write_file(&mut self, path: impl AsRef, compress: bool) -> StreamResult<()> { + let mut file = tokio::fs::File::open(path).await?; + let mut read_buf = vec![0u8; 8192]; + + if compress { + let mut encoder = + flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default()); + loop { + let n = file.read(&mut read_buf).await?; + if n == 0 { + break; + } + // Writing into a `Vec` is infallible. + encoder.write_all(&read_buf[..n]).expect("deflate write to Vec is infallible"); + // Drain whole MTU-sized chunks of compressed output as they accumulate so + // we never hold the full compressed file in memory. + while encoder.get_ref().len() >= STREAM_CHUNK_SIZE_BYTES { + let rest = encoder.get_mut().split_off(STREAM_CHUNK_SIZE_BYTES); + let chunk = std::mem::replace(encoder.get_mut(), rest); + self.write_chunk(&chunk).await?; + } + } + // Flush the final deflate block and send whatever compressed bytes remain. + let remaining = encoder.finish().expect("deflate finish into Vec is infallible"); + self.write_raw_chunks(&remaining).await?; + } else { + let mut pending: Vec = Vec::new(); + loop { + let n = file.read(&mut read_buf).await?; + if n == 0 { + break; + } + pending.extend_from_slice(&read_buf[..n]); + while pending.len() >= STREAM_CHUNK_SIZE_BYTES { + let rest = pending.split_off(STREAM_CHUNK_SIZE_BYTES); + let chunk = std::mem::replace(&mut pending, rest); + self.write_chunk(&chunk).await?; + } + } + if !pending.is_empty() { + self.write_chunk(&pending).await?; + } + } + Ok(()) + } + async fn close(&mut self, reason: Option<&str>) -> StreamResult<()> { if self.is_closed { Err(StreamError::AlreadyClosed)? @@ -263,6 +309,9 @@ pub struct StreamByteOptions { pub mime_type: Option, pub name: Option, pub total_length: Option, + /// Whether to deflate-raw compress the payload when all recipients support it. + /// Defaults to `true` (compression opt-out). Ignored by the incremental `stream_bytes`. + pub compress: Option, } /// Options used when opening an outgoing text data stream. @@ -277,6 +326,9 @@ pub struct StreamTextOptions { pub reply_to_stream_id: Option, pub attached_stream_ids: Vec, pub generated: Option, + /// Whether to deflate-raw compress the payload when all recipients support it. + /// Defaults to `true` (compression opt-out). Ignored by the incremental `stream_text`. + pub compress: Option, } #[derive(Clone)] @@ -293,31 +345,16 @@ impl OutgoingStreamManager { } pub async fn stream_text(&self, options: StreamTextOptions) -> StreamResult { - let text_header = proto::data_stream::TextHeader { - operation_type: options.operation_type.unwrap_or_default() as i32, - version: options.version.unwrap_or_default(), - reply_to_stream_id: options.reply_to_stream_id.unwrap_or_default(), - attached_stream_ids: options.attached_stream_ids, - generated: options.generated.unwrap_or_default(), - }; - let header = proto::data_stream::Header { - stream_id: options.id.unwrap_or_else(|| create_random_uuid()), - timestamp: Utc::now().timestamp_millis(), - topic: options.topic, - mime_type: TEXT_MIME_TYPE.to_owned(), - total_length: None, - encryption_type: proto::encryption::Type::None.into(), - attributes: options.attributes, - content_header: Some(proto::data_stream::header::ContentHeader::TextHeader( - text_header.clone(), - )), - // Data streams v2 fields - inline_content: None, - compression: proto::data_stream::CompressionType::None as i32, - }; + // Incremental streams are never inlined or compressed (the content is unknown up front). + let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); + let dests = options.destination_identities.clone(); + let (header, text_header) = + build_text_header(&options, stream_id, None, None, CompressionType::None); + enforce_header_size(&header, &dests)?; + let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: options.destination_identities, + destination_identities: dests, packet_tx: self.packet_tx.clone(), }; let writer = TextStreamWriter { @@ -328,26 +365,22 @@ impl OutgoingStreamManager { } pub async fn stream_bytes(&self, options: StreamByteOptions) -> StreamResult { - let byte_header = proto::data_stream::ByteHeader { name: options.name.unwrap_or_default() }; - let header = proto::data_stream::Header { - stream_id: options.id.unwrap_or_else(|| create_random_uuid()), - timestamp: Utc::now().timestamp_millis(), - topic: options.topic, - mime_type: options.mime_type.unwrap_or_else(|| BYTE_MIME_TYPE.to_owned()), - total_length: options.total_length, - encryption_type: proto::encryption::Type::None.into(), - attributes: options.attributes, - content_header: Some(proto::data_stream::header::ContentHeader::ByteHeader( - byte_header.clone(), - )), - // Data streams v2 fields - inline_content: None, - compression: proto::data_stream::CompressionType::None as i32, - }; + let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); + let name = options.name.clone().unwrap_or_default(); + let dests = options.destination_identities.clone(); + let (header, byte_header) = build_byte_header( + &options, + stream_id, + name, + options.total_length, + None, + CompressionType::None, + ); + enforce_header_size(&header, &dests)?; let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: options.destination_identities, + destination_identities: dests, packet_tx: self.packet_tx.clone(), }; let writer = ByteStreamWriter { @@ -361,43 +394,56 @@ impl OutgoingStreamManager { &self, text: &str, options: StreamTextOptions, + registry: &dyn RemoteParticipantRegistry, ) -> StreamResult { - let text_header = proto::data_stream::TextHeader { - operation_type: options.operation_type.unwrap_or_default() as i32, - version: options.version.unwrap_or_default(), - reply_to_stream_id: options.reply_to_stream_id.unwrap_or_default(), - attached_stream_ids: options.attached_stream_ids, - generated: options.generated.unwrap_or_default(), - }; - let header = proto::data_stream::Header { - stream_id: options.id.unwrap_or_else(|| create_random_uuid()), - timestamp: Utc::now().timestamp_millis(), - topic: options.topic, - mime_type: TEXT_MIME_TYPE.to_owned(), - total_length: Some(text.bytes().len() as u64), - encryption_type: proto::encryption::Type::None.into(), - attributes: options.attributes, - content_header: Some(proto::data_stream::header::ContentHeader::TextHeader( - text_header.clone(), - )), - // Data streams v2 fields - inline_content: None, - compression: proto::data_stream::CompressionType::None as i32, - }; + let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); + let total_length = text.len() as u64; + let payload = text.as_bytes(); + let dests = options.destination_identities.clone(); + + let eligibility = evaluate_eligibility(registry, &dests); + let compress_ok = options.compress.unwrap_or(true) && eligibility.compression; + + // 1. Inline single-packet attempt (no attachments; all recipients are v2). + if eligibility.inline && options.attached_stream_ids.is_empty() { + let (content, compression) = maybe_compress_inline(payload, compress_ok); + let (header, text_header) = build_text_header( + &options, + stream_id.clone(), + Some(total_length), + Some(content), + compression, + ); + if header_packet_fits(&header, &dests) { + let packet = RawStream::create_header_packet(header.clone(), dests); + RawStream::send_packet(&self.packet_tx, packet).await?; + return Ok(TextStreamInfo::from_headers(header, text_header)); + } + // Otherwise (large payload), fall through to the chunked path. + } + + // 2/3. Chunked, compressed when eligible else uncompressed. + let compression = + if compress_ok { CompressionType::DeflateRaw } else { CompressionType::None }; + let (header, text_header) = + build_text_header(&options, stream_id, Some(total_length), None, compression); + enforce_header_size(&header, &dests)?; + let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: options.destination_identities, + destination_identities: dests, packet_tx: self.packet_tx.clone(), }; - let writer = TextStreamWriter { - info: Arc::new(TextStreamInfo::from_headers(header, text_header)), - stream: Arc::new(Mutex::new(RawStream::open(open_options).await?)), - }; - - let info = (*writer.info).clone(); - writer.write(text).await?; - writer.close().await?; - + let info = TextStreamInfo::from_headers(header, text_header); + let mut stream = RawStream::open(open_options).await?; + if compress_ok { + stream.write_raw_chunks(&deflate_raw(payload)).await?; + } else { + for chunk in payload.utf8_aware_chunks(STREAM_CHUNK_SIZE_BYTES) { + stream.write_chunk(chunk).await?; + } + } + stream.close(None).await?; Ok(info) } @@ -408,100 +454,240 @@ impl OutgoingStreamManager { /// entire buffer, and closes the stream before returning. /// /// The `total_length` in the header is set from the provided data and is not - /// overridable by `options.total_length`. + /// overridable by `options.total_length`. The header defaults `name` to `"unknown"` + /// and `mime_type` to `"application/octet-stream"`. pub async fn send_bytes( &self, data: impl AsRef<[u8]>, options: StreamByteOptions, + registry: &dyn RemoteParticipantRegistry, ) -> StreamResult { if options.total_length.is_some() { log::warn!("Ignoring total_length option specified for send_bytes"); } let bytes = data.as_ref(); + let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); + let name = options.name.clone().unwrap_or_else(|| BYTE_DEFAULT_NAME.to_owned()); + let total_length = bytes.len() as u64; + let dests = options.destination_identities.clone(); - let byte_header = proto::data_stream::ByteHeader { name: options.name.unwrap_or_default() }; - let header = proto::data_stream::Header { - stream_id: options.id.unwrap_or_else(|| create_random_uuid()), - timestamp: Utc::now().timestamp_millis(), - topic: options.topic, - mime_type: options.mime_type.unwrap_or_else(|| BYTE_MIME_TYPE.to_owned()), - total_length: Some(bytes.len() as u64), // not overridable - encryption_type: proto::encryption::Type::None.into(), - attributes: options.attributes, - content_header: Some(proto::data_stream::header::ContentHeader::ByteHeader( - byte_header.clone(), - )), - // Data streams v2 fields - inline_content: None, - compression: proto::data_stream::CompressionType::None as i32, - }; + let eligibility = evaluate_eligibility(registry, &dests); + let compress_ok = options.compress.unwrap_or(true) && eligibility.compression; + + // 1. Inline single-packet attempt (all recipients are v2). + if eligibility.inline { + let (content, compression) = maybe_compress_inline(bytes, compress_ok); + let (header, byte_header) = build_byte_header( + &options, + stream_id.clone(), + name.clone(), + Some(total_length), + Some(content), + compression, + ); + if header_packet_fits(&header, &dests) { + let packet = RawStream::create_header_packet(header.clone(), dests); + RawStream::send_packet(&self.packet_tx, packet).await?; + return Ok(ByteStreamInfo::from_headers(header, byte_header)); + } + } + + // 2/3. Chunked, compressed when eligible else uncompressed. + let compression = + if compress_ok { CompressionType::DeflateRaw } else { CompressionType::None }; + let (header, byte_header) = + build_byte_header(&options, stream_id, name, Some(total_length), None, compression); + enforce_header_size(&header, &dests)?; let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: options.destination_identities, + destination_identities: dests, packet_tx: self.packet_tx.clone(), }; - let writer = ByteStreamWriter { - info: Arc::new(ByteStreamInfo::from_headers(header, byte_header)), - stream: Arc::new(Mutex::new(RawStream::open(open_options).await?)), - }; - - let info = (*writer.info).clone(); - writer.write(bytes).await?; - writer.close().await?; - + let info = ByteStreamInfo::from_headers(header, byte_header); + let mut stream = RawStream::open(open_options).await?; + if compress_ok { + stream.write_raw_chunks(&deflate_raw(bytes)).await?; + } else { + stream.write_raw_chunks(bytes).await?; + } + stream.close(None).await?; Ok(info) } + /// Streams a file from disk to participants as a byte stream. + /// + /// Never uses the inline single-packet path (deciding inline-eligibility would require + /// buffering and compressing the whole file up front). Compresses when every recipient + /// supports it. The whole file is never buffered in memory at once. pub async fn send_file( &self, path: impl AsRef, options: StreamByteOptions, + registry: &dyn RemoteParticipantRegistry, ) -> StreamResult { - let file_size = tokio::fs::metadata(path.as_ref()) + let path = path.as_ref(); + let file_size = tokio::fs::metadata(path) .await .map(|metadata| metadata.len()) - .map_err(|e| StreamError::from(e))?; - let name = - path.as_ref().file_name().and_then(|n| n.to_str()).unwrap_or_default().to_owned(); - - let byte_header = proto::data_stream::ByteHeader { name }; - let header = proto::data_stream::Header { - stream_id: options.id.unwrap_or_else(|| create_random_uuid()), - timestamp: Utc::now().timestamp_millis(), - topic: options.topic, - mime_type: options.mime_type.unwrap_or_else(|| BYTE_MIME_TYPE.to_owned()), - total_length: Some(file_size as u64), // not overridable - encryption_type: proto::encryption::Type::None.into(), - attributes: options.attributes, - content_header: Some(proto::data_stream::header::ContentHeader::ByteHeader( - byte_header.clone(), - )), - // Data streams v2 fields - inline_content: None, - compression: proto::data_stream::CompressionType::None as i32, - }; + .map_err(StreamError::from)?; + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default().to_owned(); + let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); + let dests = options.destination_identities.clone(); + + let eligibility = evaluate_eligibility(registry, &dests); + let compress_ok = options.compress.unwrap_or(true) && eligibility.compression; + let compression = + if compress_ok { CompressionType::DeflateRaw } else { CompressionType::None }; + + let (header, byte_header) = + build_byte_header(&options, stream_id, name, Some(file_size), None, compression); + enforce_header_size(&header, &dests)?; let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: options.destination_identities, + destination_identities: dests, packet_tx: self.packet_tx.clone(), }; - let writer = ByteStreamWriter { - info: Arc::new(ByteStreamInfo::from_headers(header, byte_header)), - stream: Arc::new(Mutex::new(RawStream::open(open_options).await?)), - }; + let info = ByteStreamInfo::from_headers(header, byte_header); + let mut stream = RawStream::open(open_options).await?; + stream.write_file(path, compress_ok).await?; + stream.close(None).await?; + Ok(info) + } +} - let info = (*writer.info).clone(); - writer.write_file_contents(path).await?; - writer.close().await?; +/// Inline / compression eligibility evaluated over a send's recipients. +struct SendEligibility { + /// Every recipient advertises `clientProtocol >= 2`. + inline: bool, + /// Inline-eligible AND every recipient advertises `CAP_COMPRESSION_DEFLATE_RAW`. + compression: bool, +} - Ok(info) +/// Evaluates inline/compression eligibility over a send's recipients. +/// +/// Recipients are the named `destinations`, or every remote participant for a broadcast +/// (empty `destinations`). An empty recipient set (empty room) is eligible for everything. +fn evaluate_eligibility( + registry: &dyn RemoteParticipantRegistry, + destinations: &[ParticipantIdentity], +) -> SendEligibility { + let recipients: Vec = + if destinations.is_empty() { registry.remote_identities() } else { destinations.to_vec() }; + let inline = recipients + .iter() + .all(|id| registry.remote_client_protocol(id) >= CLIENT_PROTOCOL_DATA_STREAM_V2); + let compression = inline + && recipients.iter().all(|id| { + registry.remote_capabilities(id).contains(&ClientCapability::CompressionDeflateRaw) + }); + SendEligibility { inline, compression } +} + +/// Returns the inline payload and its compression flag: deflate-raw compressed when +/// `compress` is set AND the compressed form is actually smaller, else the raw bytes. +fn maybe_compress_inline(payload: &[u8], compress: bool) -> (Vec, CompressionType) { + if compress { + let compressed = deflate_raw(payload); + if compressed.len() < payload.len() { + return (compressed, CompressionType::DeflateRaw); + } + } + (payload.to_vec(), CompressionType::None) +} + +/// One-shot deflate-raw (raw DEFLATE, no zlib/gzip wrapper) of the full payload. +fn deflate_raw(data: &[u8]) -> Vec { + let mut encoder = + flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default()); + encoder.write_all(data).expect("deflate write to Vec is infallible"); + encoder.finish().expect("deflate finish into Vec is infallible") +} + +/// Whether the serialized header `DataPacket` fits within the MTU budget. +fn header_packet_fits( + header: &proto::data_stream::Header, + destinations: &[ParticipantIdentity], +) -> bool { + use prost::Message; + let packet = RawStream::create_header_packet(header.clone(), destinations.to_vec()); + packet.encoded_len() <= STREAM_CHUNK_SIZE_BYTES +} + +/// Enforces the header-packet MTU budget on the chunked path (the inline path falls back +/// gracefully instead of erroring). +fn enforce_header_size( + header: &proto::data_stream::Header, + destinations: &[ParticipantIdentity], +) -> StreamResult<()> { + if header_packet_fits(header, destinations) { + Ok(()) + } else { + Err(StreamError::HeaderTooLarge) } } -/// Maximum number of bytes to send in a single chunk. -static CHUNK_SIZE: usize = 15000; +fn build_text_header( + options: &StreamTextOptions, + stream_id: String, + total_length: Option, + inline_content: Option>, + compression: CompressionType, +) -> (proto::data_stream::Header, proto::data_stream::TextHeader) { + let text_header = proto::data_stream::TextHeader { + operation_type: options.operation_type.unwrap_or_default() as i32, + version: options.version.unwrap_or_default(), + reply_to_stream_id: options.reply_to_stream_id.clone().unwrap_or_default(), + attached_stream_ids: options.attached_stream_ids.clone(), + generated: options.generated.unwrap_or_default(), + }; + let header = proto::data_stream::Header { + stream_id, + timestamp: Utc::now().timestamp_millis(), + topic: options.topic.clone(), + mime_type: TEXT_MIME_TYPE.to_owned(), + total_length, + encryption_type: proto::encryption::Type::None.into(), + attributes: options.attributes.clone(), + content_header: Some(proto::data_stream::header::ContentHeader::TextHeader( + text_header.clone(), + )), + inline_content, + compression: compression as i32, + }; + (header, text_header) +} + +fn build_byte_header( + options: &StreamByteOptions, + stream_id: String, + name: String, + total_length: Option, + inline_content: Option>, + compression: CompressionType, +) -> (proto::data_stream::Header, proto::data_stream::ByteHeader) { + let byte_header = proto::data_stream::ByteHeader { name }; + let header = proto::data_stream::Header { + stream_id, + timestamp: Utc::now().timestamp_millis(), + topic: options.topic.clone(), + mime_type: options.mime_type.clone().unwrap_or_else(|| BYTE_MIME_TYPE.to_owned()), + total_length, + encryption_type: proto::encryption::Type::None.into(), + attributes: options.attributes.clone(), + content_header: Some(proto::data_stream::header::ContentHeader::ByteHeader( + byte_header.clone(), + )), + inline_content, + compression: compression as i32, + }; + (header, byte_header) +} + +/// Max chunk content size AND the header-packet MTU budget. Kept below the ~16 KB +/// data-channel MTU for protocol/E2EE framing headroom. +const STREAM_CHUNK_SIZE_BYTES: usize = 15000; // Default MIME type to use for byte streams. static BYTE_MIME_TYPE: &str = "application/octet-stream"; @@ -509,9 +695,449 @@ static BYTE_MIME_TYPE: &str = "application/octet-stream"; /// Default MIME type to use for text streams. static TEXT_MIME_TYPE: &str = "text/plain"; +/// Default name for `send_bytes` byte-stream headers. +static BYTE_DEFAULT_NAME: &str = "unknown"; + #[cfg(test)] mod tests { use super::*; + use std::sync::Mutex as StdMutex; + + const V2: i32 = CLIENT_PROTOCOL_DATA_STREAM_V2; + const DEFLATE: ClientCapability = ClientCapability::CompressionDeflateRaw; + + // --- Fake recipient registry --------------------------------------------------------- + + struct FakeRegistry { + remotes: HashMap)>, + } + + impl FakeRegistry { + fn new() -> Self { + Self { remotes: HashMap::new() } + } + + fn add(mut self, id: &str, client_protocol: i32, caps: &[ClientCapability]) -> Self { + self.remotes.insert(id.to_string(), (client_protocol, caps.to_vec())); + self + } + } + + impl RemoteParticipantRegistry for FakeRegistry { + fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { + self.remotes.get(&identity.0).map(|(p, _)| *p).unwrap_or(0) + } + fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec { + self.remotes.get(&identity.0).map(|(_, c)| c.clone()).unwrap_or_default() + } + fn remote_identities(&self) -> Vec { + self.remotes.keys().map(|k| ParticipantIdentity(k.clone())).collect() + } + } + + fn pre_v2_room() -> FakeRegistry { + FakeRegistry::new().add("alice", 0, &[]).add("bob", 0, &[]).add("jim", 1, &[]) + } + + fn all_v2_room() -> FakeRegistry { + FakeRegistry::new().add("alice", V2, &[DEFLATE]).add("bob", V2, &[DEFLATE]).add( + "noCompression", + V2, + &[], + ) + } + + fn mixed_room() -> FakeRegistry { + FakeRegistry::new() + .add("alice", 0, &[]) + .add("bob", V2, &[DEFLATE]) + .add("jim", V2, &[DEFLATE]) + .add("mallory", 1, &[]) + .add("noCompression", V2, &[]) + } + + // --- Capture harness ----------------------------------------------------------------- + + type Sent = Arc>>; + + fn setup() -> (OutgoingStreamManager, Sent) { + let (manager, mut packet_rx) = OutgoingStreamManager::new(); + let sent: Sent = Arc::new(StdMutex::new(Vec::new())); + let sink = sent.clone(); + tokio::spawn(async move { + while let Ok((packet, responder)) = packet_rx.recv().await { + sink.lock().unwrap().push(packet); + let _ = responder.respond(Ok(())); + } + }); + (manager, sent) + } + + fn ids(list: &[&str]) -> Vec { + list.iter().map(|s| ParticipantIdentity(s.to_string())).collect() + } + + fn text_opts(topic: &str, dests: &[&str]) -> StreamTextOptions { + StreamTextOptions { + topic: topic.to_string(), + destination_identities: ids(dests), + ..Default::default() + } + } + + fn byte_opts(topic: &str, dests: &[&str]) -> StreamByteOptions { + StreamByteOptions { + topic: topic.to_string(), + destination_identities: ids(dests), + ..Default::default() + } + } + + fn header(p: &proto::DataPacket) -> &proto::data_stream::Header { + match p.value.as_ref().unwrap() { + proto::data_packet::Value::StreamHeader(h) => h, + _ => panic!("expected stream header"), + } + } + + fn chunk(p: &proto::DataPacket) -> &proto::data_stream::Chunk { + match p.value.as_ref().unwrap() { + proto::data_packet::Value::StreamChunk(c) => c, + _ => panic!("expected stream chunk"), + } + } + + fn is_text_header(h: &proto::data_stream::Header) -> bool { + matches!(h.content_header, Some(proto::data_stream::header::ContentHeader::TextHeader(_))) + } + + fn is_byte_header(h: &proto::data_stream::Header) -> bool { + matches!(h.content_header, Some(proto::data_stream::header::ContentHeader::ByteHeader(_))) + } + + fn assert_trailer(p: &proto::DataPacket) { + match p.value.as_ref().unwrap() { + proto::data_packet::Value::StreamTrailer(t) => assert_eq!(t.reason, ""), + _ => panic!("expected stream trailer"), + } + } + + fn deflate_raw_i32() -> i32 { + CompressionType::DeflateRaw as i32 + } + fn none_i32() -> i32 { + CompressionType::None as i32 + } + + /// ~50 KB of deterministic, somewhat-compressible text (repeated marker + pseudo-random + /// lowercase). Compresses to >15 KB (so it can't inline) but well under its raw size. + fn somewhat_compressible(blocks: usize) -> String { + let mut s = String::new(); + let mut state: u64 = 0x1234_5678_9abc_def0; + for _ in 0..blocks { + s.push_str("hello world"); + for _ in 0..1000 { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + s.push((b'a' + ((state >> 33) % 26) as u8) as char); + } + } + s + } + + // --- Pre-v2 room: legacy, uncompressed, multi-packet --------------------------------- + + #[tokio::test] + async fn pre_v2_short_text_is_legacy_multipacket() { + let (m, sent) = setup(); + m.send_text("hello world", text_opts("chat", &[]), &pre_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 3); + let h = header(&p[0]); + assert!(is_text_header(h)); + assert_eq!(h.topic, "chat"); + assert_eq!(h.compression, none_i32()); + assert!(h.inline_content.is_none()); + let c = chunk(&p[1]); + assert_eq!(c.chunk_index, 0); + assert_eq!(c.content, b"hello world"); + assert_trailer(&p[2]); + } + + #[tokio::test] + async fn pre_v2_long_text_splits_at_mtu() { + let (m, sent) = setup(); + let text = "A".repeat(40_000); + m.send_text(&text, text_opts("chat", &[]), &pre_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 5); // header + 3 chunks + trailer + assert_eq!(header(&p[0]).compression, none_i32()); + assert_eq!(chunk(&p[1]).content.len(), 15_000); + assert_eq!(chunk(&p[2]).content.len(), 15_000); + assert_eq!(chunk(&p[3]).content.len(), 10_000); + assert_eq!(chunk(&p[1]).chunk_index, 0); + assert_eq!(chunk(&p[3]).chunk_index, 2); + assert_trailer(&p[4]); + } + + #[tokio::test] + async fn pre_v2_bytes_is_legacy_multipacket() { + let (m, sent) = setup(); + m.send_bytes([0u8, 1, 2, 3], byte_opts("blob", &[]), &pre_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 3); + let h = header(&p[0]); + assert!(is_byte_header(h)); + assert_eq!(h.compression, none_i32()); + assert!(h.inline_content.is_none()); + assert_eq!(chunk(&p[1]).content, vec![0, 1, 2, 3]); + assert_trailer(&p[2]); + } + + // --- All-v2 room: inline + compression ----------------------------------------------- + + #[tokio::test] + async fn v2_short_compressible_text_inlines_compressed() { + let (m, sent) = setup(); + let text = "hello hello compressible world"; + m.send_text(text, text_opts("chat", &["alice", "bob"]), &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert!(is_text_header(h)); + assert_eq!(h.compression, deflate_raw_i32()); + let inline = h.inline_content.as_ref().unwrap(); + assert_ne!(inline.as_slice(), text.as_bytes()); // compressed, not raw + } + + #[tokio::test] + async fn v2_short_incompressible_text_inlines_raw() { + let (m, sent) = setup(); + m.send_text("short", text_opts("chat", &["alice", "bob"]), &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert_eq!(h.compression, none_i32()); + assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), b"short"); + } + + #[tokio::test] + async fn v2_no_compression_cap_inlines_raw() { + let (m, sent) = setup(); + let text = "hello hello compressible world"; + m.send_text(text, text_opts("chat", &["noCompression"]), &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); // inline (gated on protocol) still happens + let h = header(&p[0]); + assert_eq!(h.compression, none_i32()); // compression gated off by missing cap + assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes()); + } + + #[tokio::test] + async fn v2_large_highly_compressible_text_still_inlines() { + let (m, sent) = setup(); + let text = "hello world".repeat(20_000); + m.send_text(&text, text_opts("chat", &["alice", "bob"]), &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert_eq!(h.compression, deflate_raw_i32()); + assert!(h.inline_content.as_ref().unwrap().len() < text.len()); + } + + #[tokio::test] + async fn v2_somewhat_compressible_text_is_compressed_multipacket() { + let (m, sent) = setup(); + let text = somewhat_compressible(50); + m.send_text(&text, text_opts("chat", &["alice", "bob"]), &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + let h = header(&p[0]); + assert_eq!(h.compression, deflate_raw_i32()); + assert!(h.inline_content.is_none()); + let chunks: Vec<_> = p[1..p.len() - 1].iter().map(chunk).collect(); + // Multi-packet, but fewer chunks than an uncompressed send would need (ceil(len/15000)). + let uncompressed_chunks = text.len().div_ceil(STREAM_CHUNK_SIZE_BYTES); + assert!(chunks.len() >= 2); + assert!(chunks.len() < uncompressed_chunks); + assert_eq!(chunks[0].content.len(), STREAM_CHUNK_SIZE_BYTES); // first chunk is full MTU + let total: usize = chunks.iter().map(|c| c.content.len()).sum(); + assert!(total < text.len()); // compressed + assert_trailer(p.last().unwrap()); + } + + #[tokio::test] + async fn v2_compress_false_short_inlines_raw() { + let (m, sent) = setup(); + let text = "hello hello compressible world"; + let opts = + StreamTextOptions { compress: Some(false), ..text_opts("chat", &["alice", "bob"]) }; + m.send_text(text, opts, &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert_eq!(h.compression, none_i32()); + assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes()); + } + + #[tokio::test] + async fn v2_compress_false_large_is_uncompressed_multipacket() { + let (m, sent) = setup(); + let text = "B".repeat(50_000); + let opts = + StreamTextOptions { compress: Some(false), ..text_opts("chat", &["alice", "bob"]) }; + m.send_text(&text, opts, &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 6); // header + 4 chunks + trailer + assert_eq!(header(&p[0]).compression, none_i32()); + assert_eq!(chunk(&p[1]).content.len(), 15_000); + } + + // --- Incremental writers never compress or inline ------------------------------------ + + #[tokio::test] + async fn stream_text_never_compresses_or_inlines() { + let (m, sent) = setup(); + let writer = m.stream_text(text_opts("chat", &["noCompression"])).await.unwrap(); + assert_eq!(sent.lock().unwrap().len(), 1); + let h0 = sent.lock().unwrap()[0].clone(); + assert!(is_text_header(header(&h0))); + assert_eq!(header(&h0).compression, none_i32()); + assert!(header(&h0).inline_content.is_none()); + + writer.write("hello world").await.unwrap(); + assert_eq!(sent.lock().unwrap().len(), 2); + assert_eq!(chunk(&sent.lock().unwrap()[1]).content, b"hello world"); + + writer.close().await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 3); + assert_trailer(&p[2]); + } + + #[tokio::test] + async fn stream_bytes_never_compresses_or_inlines() { + let (m, sent) = setup(); + let writer = m.stream_bytes(byte_opts("blob", &["noCompression"])).await.unwrap(); + assert_eq!(sent.lock().unwrap().len(), 1); + assert_eq!(header(&sent.lock().unwrap()[0]).compression, none_i32()); + + writer.write(&[0u8, 1, 2, 3]).await.unwrap(); + assert_eq!(chunk(&sent.lock().unwrap()[1]).content, vec![0, 1, 2, 3]); + + writer.close().await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 3); + assert_trailer(&p[2]); + } + + // --- send_bytes inline behavior ------------------------------------------------------ + + #[tokio::test] + async fn v2_send_bytes_short_compressible_inlines_compressed() { + let (m, sent) = setup(); + let payload = "hello hello compressible world".as_bytes().to_vec(); + let mut opts = byte_opts("blob", &["alice", "bob"]); + opts.attributes.insert("foo".to_string(), "bar".to_string()); + let info = m.send_bytes(&payload, opts, &all_v2_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert!(is_byte_header(h)); + assert_eq!(h.compression, deflate_raw_i32()); + assert_ne!(h.inline_content.as_ref().unwrap().as_slice(), payload.as_slice()); + assert_eq!(info.name, "unknown"); + assert_eq!(info.mime_type, "application/octet-stream"); + assert_eq!(info.total_length, Some(payload.len() as u64)); + assert_eq!(info.attributes.get("foo"), Some(&"bar".to_string())); + } + + // --- Mixed room ---------------------------------------------------------------------- + + #[tokio::test] + async fn mixed_broadcast_falls_back_to_legacy() { + let (m, sent) = setup(); + m.send_text("hello world", text_opts("chat", &[]), &mixed_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 3); + assert_eq!(header(&p[0]).compression, none_i32()); + assert!(header(&p[0]).inline_content.is_none()); + assert_eq!(chunk(&p[1]).content, b"hello world"); + } + + #[tokio::test] + async fn mixed_targeted_v2_subset_inlines_compressed() { + let (m, sent) = setup(); + let text = "hello hello compressible world"; + m.send_text(text, text_opts("chat", &["bob", "jim"]), &mixed_room()).await.unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert_eq!(h.compression, deflate_raw_i32()); + assert_ne!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes()); + } + + #[tokio::test] + async fn mixed_targeted_subset_missing_cap_inlines_uncompressed() { + let (m, sent) = setup(); + let text = "hello hello compressible world"; + m.send_text(text, text_opts("chat", &["bob", "jim", "noCompression"]), &mixed_room()) + .await + .unwrap(); + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 1); + let h = header(&p[0]); + assert_eq!(h.compression, none_i32()); + assert_eq!(h.inline_content.as_ref().unwrap().as_slice(), text.as_bytes()); + } + + // --- send_file ----------------------------------------------------------------------- + + async fn write_temp_file(bytes: &[u8]) -> std::path::PathBuf { + let path = std::env::temp_dir().join(format!("lk_ds_test_{}.bin", create_random_uuid())); + tokio::fs::write(&path, bytes).await.unwrap(); + path + } + + #[tokio::test] + async fn send_file_never_inlines_and_compresses_when_eligible() { + let (m, sent) = setup(); + let path = write_temp_file(&vec![0x01u8; 10_000]).await; + m.send_file(&path, byte_opts("file", &["alice", "bob"]), &all_v2_room()).await.unwrap(); + let _ = tokio::fs::remove_file(&path).await; + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 3); // header + 1 chunk + trailer, NOT inline + let h = header(&p[0]); + assert!(is_byte_header(h)); + assert_eq!(h.compression, deflate_raw_i32()); + assert!(h.inline_content.is_none()); + assert!(chunk(&p[1]).content.len() < 10_000); // compressed + assert_trailer(&p[2]); + } + + #[tokio::test] + async fn send_file_uncompressed_splits_at_mtu() { + let (m, sent) = setup(); + let path = write_temp_file(&vec![0x07u8; 20_000]).await; + m.send_file(&path, byte_opts("file", &[]), &pre_v2_room()).await.unwrap(); + let _ = tokio::fs::remove_file(&path).await; + let p = sent.lock().unwrap().clone(); + assert_eq!(p.len(), 4); // header + 15000 + 5000 + trailer + assert_eq!(header(&p[0]).compression, none_i32()); + assert_eq!(chunk(&p[1]).content.len(), 15_000); + assert_eq!(chunk(&p[2]).content.len(), 5_000); + assert_eq!(chunk(&p[2]).chunk_index, 1); + assert_trailer(&p[3]); + } + + // --- Header size limit --------------------------------------------------------------- + + #[tokio::test] + async fn oversized_attributes_on_chunked_path_errors() { + let (m, _sent) = setup(); + let mut opts = text_opts("chat", &[]); // pre-v2 below => chunked path + opts.attributes.insert("big".to_string(), "x".repeat(20_000)); + let result = m.send_text("hello", opts, &pre_v2_room()).await; + assert!(matches!(result, Err(StreamError::HeaderTooLarge))); + } // Regression test for CLT-2773: dropping a `RawStream` on a thread that has // no Tokio runtime in TLS (e.g. the .NET GC finalizer thread in the Unity diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index c84e42538..b7e38b181 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -48,7 +48,7 @@ use tokio::sync::{ pub use self::{ data_stream::*, e2ee::{manager::E2eeManager, E2eeOptions}, - participant::{ParticipantKind, ParticipantKindDetail, ParticipantState}, + participant::{ClientCapability, ParticipantKind, ParticipantKindDetail, ParticipantState}, }; pub use crate::rtc_engine::SimulateScenario; use crate::{ @@ -577,6 +577,7 @@ impl Room { e2ee_manager.encryption_type(), pi.permission, pi.client_protocol, + pi.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(), ); let dispatcher = Dispatcher::::default(); @@ -759,6 +760,7 @@ impl Room { pi.joined_at_ms, pi.permission, pi.client_protocol, + pi.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(), ) }; participant.update_info(pi.clone()); @@ -1195,6 +1197,7 @@ impl RoomSession { pi.joined_at_ms, pi.permission, pi.client_protocol, + pi.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(), ) }; @@ -2000,6 +2003,7 @@ impl RoomSession { joined_at: i64, permission: Option, client_protocol: i32, + capabilities: Vec, ) -> RemoteParticipant { let participant = RemoteParticipant::new( self.rtc_engine.clone(), @@ -2015,6 +2019,7 @@ impl RoomSession { self.options.auto_subscribe, permission, client_protocol, + capabilities, ); participant.on_track_published({ @@ -2233,6 +2238,20 @@ impl RoomSession { } } +impl rpc::RemoteParticipantRegistry for RoomSession { + fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { + self.get_remote_client_protocol(identity) + } + + fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec { + self.remote_participants.read().get(identity).map(|p| p.capabilities()).unwrap_or_default() + } + + fn remote_identities(&self) -> Vec { + self.remote_participants.read().keys().cloned().collect() + } +} + /// Receives stream readers for newly-opened streams and dispatches room events. /// /// Intercepts text streams on RPC topics (`lk.rpc_request`, `lk.rpc_response`) diff --git a/livekit/src/room/participant/local_participant.rs b/livekit/src/room/participant/local_participant.rs index 12c4ddad9..a5be6f0e8 100644 --- a/livekit/src/room/participant/local_participant.rs +++ b/livekit/src/room/participant/local_participant.rs @@ -23,8 +23,8 @@ use std::{ }; use super::{ - ConnectionQuality, ParticipantInner, ParticipantKind, ParticipantKindDetail, ParticipantState, - ParticipantTrackPermission, + ClientCapability, ConnectionQuality, ParticipantInner, ParticipantKind, ParticipantKindDetail, + ParticipantState, ParticipantTrackPermission, }; use crate::{ data_stream::{ @@ -110,6 +110,7 @@ impl LocalParticipant { encryption_type: EncryptionType, permission: Option, client_protocol: i32, + capabilities: Vec, ) -> Self { Self { inner: super::new_inner( @@ -125,6 +126,7 @@ impl LocalParticipant { joined_at, permission, client_protocol, + capabilities, ), local: Arc::new(LocalInfo { events: LocalEvents::default(), @@ -910,7 +912,8 @@ impl LocalParticipant { text: &str, options: StreamTextOptions, ) -> StreamResult { - self.session().unwrap().outgoing_stream_manager.send_text(text, options).await + let session = self.session().unwrap(); + session.outgoing_stream_manager.send_text(text, options, session.as_ref()).await } /// Send a file on disk to participants in the room. @@ -930,7 +933,8 @@ impl LocalParticipant { path: impl AsRef, options: StreamByteOptions, ) -> StreamResult { - self.session().unwrap().outgoing_stream_manager.send_file(path, options).await + let session = self.session().unwrap(); + session.outgoing_stream_manager.send_file(path, options, session.as_ref()).await } /// Send an in-memory blob of bytes to participants in the room. @@ -947,7 +951,8 @@ impl LocalParticipant { data: impl AsRef<[u8]>, options: StreamByteOptions, ) -> StreamResult { - self.session().unwrap().outgoing_stream_manager.send_bytes(data, options).await + let session = self.session().unwrap(); + session.outgoing_stream_manager.send_bytes(data, options, session.as_ref()).await } /// Stream text incrementally to participants in the room. diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index 5dcdc83d7..1ff2f6b58 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -85,6 +85,44 @@ pub enum DisconnectReason { AgentError, } +/// A capability a participant's client advertises (mirrors `ClientInfo.Capability`). +/// +/// Stored typed rather than as the raw protobuf `i32` so the public accessor doesn't leak +/// protobuf types, while `Unknown` preserves values this SDK build doesn't recognize. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ClientCapability { + PacketTrailer, + CompressionDeflateRaw, + Unknown(i32), +} + +impl From for ClientCapability { + fn from(value: i32) -> Self { + match proto::client_info::Capability::try_from(value) { + Ok(proto::client_info::Capability::CapPacketTrailer) => Self::PacketTrailer, + Ok(proto::client_info::Capability::CapCompressionDeflateRaw) => { + Self::CompressionDeflateRaw + } + // `CapUnused` and any value not recognized by this build. + _ => Self::Unknown(value), + } + } +} + +impl From for i32 { + fn from(value: ClientCapability) -> Self { + match value { + ClientCapability::PacketTrailer => { + proto::client_info::Capability::CapPacketTrailer as i32 + } + ClientCapability::CompressionDeflateRaw => { + proto::client_info::Capability::CapCompressionDeflateRaw as i32 + } + ClientCapability::Unknown(value) => value, + } + } +} + #[derive(Debug, Clone)] pub enum Participant { Local(LocalParticipant), @@ -146,6 +184,7 @@ struct ParticipantInfo { pub joined_at: i64, pub permission: Option, pub client_protocol: i32, + pub capabilities: Vec, } type TrackMutedHandler = Box; @@ -197,6 +236,7 @@ pub(super) fn new_inner( joined_at: i64, permission: Option, client_protocol: i32, + capabilities: Vec, ) -> Arc { Arc::new(ParticipantInner { rtc_engine, @@ -216,6 +256,7 @@ pub(super) fn new_inner( joined_at, permission, client_protocol, + capabilities, }), track_publications: Default::default(), events: Default::default(), @@ -269,6 +310,7 @@ pub(super) fn update_info( } info.client_protocol = new_info.client_protocol; + info.capabilities = new_info.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(); } pub(super) fn set_speaking( diff --git a/livekit/src/room/participant/remote_participant.rs b/livekit/src/room/participant/remote_participant.rs index da0f6a663..618da0cca 100644 --- a/livekit/src/room/participant/remote_participant.rs +++ b/livekit/src/room/participant/remote_participant.rs @@ -25,8 +25,8 @@ use livekit_runtime::timeout; use parking_lot::Mutex; use super::{ - ConnectionQuality, ParticipantInner, ParticipantKind, ParticipantKindDetail, ParticipantState, - TrackKind, + ClientCapability, ConnectionQuality, ParticipantInner, ParticipantKind, ParticipantKindDetail, + ParticipantState, TrackKind, }; use crate::{prelude::*, rtc_engine::RtcEngine, track::TrackError}; @@ -86,6 +86,7 @@ impl RemoteParticipant { auto_subscribe: bool, permission: Option, client_protocol: i32, + capabilities: Vec, ) -> Self { Self { inner: super::new_inner( @@ -101,6 +102,7 @@ impl RemoteParticipant { joined_at, permission, client_protocol, + capabilities, ), remote: Arc::new(RemoteInfo { events: Default::default(), auto_subscribe }), } @@ -577,6 +579,16 @@ impl RemoteParticipant { self.inner.info.read().client_protocol } + /// The capabilities this remote participant's client advertised at join. + pub fn capabilities(&self) -> Vec { + self.inner.info.read().capabilities.clone() + } + + /// Whether this remote participant can decompress deflate-raw data streams. + pub(crate) fn supports_compression(&self) -> bool { + self.inner.info.read().capabilities.contains(&ClientCapability::CompressionDeflateRaw) + } + pub fn is_encrypted(&self) -> bool { *self.inner.is_encrypted.read() } diff --git a/livekit/src/room/rpc/mod.rs b/livekit/src/room/rpc/mod.rs index 8f14647fa..2202163c3 100644 --- a/livekit/src/room/rpc/mod.rs +++ b/livekit/src/room/rpc/mod.rs @@ -23,6 +23,7 @@ pub use server::{HandleRequestOptions, RpcServerManager}; use crate::data_stream::{StreamResult, StreamTextOptions, TextStreamInfo}; use crate::room::id::ParticipantIdentity; +use crate::room::participant::ClientCapability; use livekit_protocol::RpcError as RpcError_Proto; use std::{error::Error, fmt::Display, future::Future, time::Duration}; @@ -41,11 +42,27 @@ pub(crate) const ATTR_METHOD: &str = "lk.rpc_request_method"; pub(crate) const ATTR_RESPONSE_TIMEOUT_MS: &str = "lk.rpc_request_response_timeout_ms"; pub(crate) const ATTR_VERSION: &str = "lk.rpc_request_version"; +/// Read access to remote participants' advertised protocol and capabilities. +/// +/// Shared by the RPC transport (v1/v2 transport selection) and the data-stream send +/// path (inline / compression eligibility), so both consult a single abstraction over +/// the room's remote participants and both are unit-testable with a fake. +pub(crate) trait RemoteParticipantRegistry: Send + Sync { + /// A remote participant's `client_protocol`, or `CLIENT_PROTOCOL_DEFAULT` (0) if unknown. + fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32; + + /// A remote participant's advertised capabilities, or empty if unknown. + fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec; + + /// The identities of every remote participant, used to resolve a broadcast send. + fn remote_identities(&self) -> Vec; +} + /// Transport abstraction for RPC operations. /// /// Decouples the RPC managers from concrete engine/session types, /// enabling in-memory unit testing with a mock transport. -pub(crate) trait RpcTransport: Send + Sync { +pub(crate) trait RpcTransport: RemoteParticipantRegistry { /// Send a data packet (used for v1 RPC packets and ACKs). fn publish_data( &self, @@ -59,9 +76,6 @@ pub(crate) trait RpcTransport: Send + Sync { options: StreamTextOptions, ) -> impl Future> + Send; - /// Look up a remote participant's client_protocol value. - fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32; - /// Get the server version string, if available. fn server_version(&self) -> Option; } @@ -69,6 +83,20 @@ pub(crate) trait RpcTransport: Send + Sync { /// Production implementation of `RpcTransport` backed by a `RoomSession`. pub(crate) struct SessionTransport(pub(crate) std::sync::Arc); +impl RemoteParticipantRegistry for SessionTransport { + fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { + self.0.remote_client_protocol(identity) + } + + fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec { + self.0.remote_capabilities(identity) + } + + fn remote_identities(&self) -> Vec { + self.0.remote_identities() + } +} + impl RpcTransport for SessionTransport { async fn publish_data( &self, @@ -86,11 +114,7 @@ impl RpcTransport for SessionTransport { text: &str, options: StreamTextOptions, ) -> StreamResult { - self.0.outgoing_stream_manager.send_text(text, options).await - } - - fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { - self.0.get_remote_client_protocol(identity) + self.0.outgoing_stream_manager.send_text(text, options, self.0.as_ref()).await } fn server_version(&self) -> Option { diff --git a/livekit/src/room/rpc/tests.rs b/livekit/src/room/rpc/tests.rs index b4fc3d5e9..ce40e78c6 100644 --- a/livekit/src/room/rpc/tests.rs +++ b/livekit/src/room/rpc/tests.rs @@ -18,6 +18,7 @@ use crate::data_stream::{ }; use crate::e2ee::EncryptionType; use crate::room::id::ParticipantIdentity; +use crate::room::participant::ClientCapability; use crate::room::RoomError; use bytes::Bytes; use chrono::Utc; @@ -135,12 +136,22 @@ impl RpcTransport for MockTransport { }) } + fn server_version(&self) -> Option { + self.server_ver.clone() + } +} + +impl RemoteParticipantRegistry for MockTransport { fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { self.remote_protocols.get(&identity.0).copied().unwrap_or(CLIENT_PROTOCOL_DEFAULT) } - fn server_version(&self) -> Option { - self.server_ver.clone() + fn remote_capabilities(&self, _identity: &ParticipantIdentity) -> Vec { + Vec::new() + } + + fn remote_identities(&self) -> Vec { + self.remote_protocols.keys().map(|k| ParticipantIdentity(k.clone())).collect() } } diff --git a/livekit/tests/data_stream_test.rs b/livekit/tests/data_stream_test.rs index 911a37548..240a142e8 100644 --- a/livekit/tests/data_stream_test.rs +++ b/livekit/tests/data_stream_test.rs @@ -70,6 +70,81 @@ async fn test_send_bytes() -> Result<()> { Ok(()) } +/// End-to-end round-trip of a large, somewhat-compressible text. Both peers are Rust SDK +/// clients advertising data streams v2 + deflate-raw, so the sender compresses across multiple +/// chunks and the receiver decompresses — validating the v2 compression path on the real wire. +#[cfg(feature = "__lk-e2e-test")] +#[tokio::test] +async fn test_send_large_compressible_text() -> Result<()> { + let mut rooms = test_rooms(2).await?; + let (sending_room, _) = rooms.pop().unwrap(); + let (_, mut receiving_event_rx) = rooms.pop().unwrap(); + + // ~50 KB of deterministic pseudo-random lowercase: too big to inline, compresses well + // under its raw size, exercising the chunked-compressed path. + let mut text = String::new(); + let mut state: u64 = 0x1234_5678_9abc_def0; + for _ in 0..50_000 { + state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + text.push((b'a' + ((state >> 33) % 26) as u8) as char); + } + let expected = text.clone(); + + let send = async move { + let options = StreamTextOptions { topic: "some-topic".into(), ..Default::default() }; + sending_room.local_participant().send_text(&text, options).await?; + Ok(()) + }; + let receive = async move { + while let Some(event) = receiving_event_rx.recv().await { + let RoomEvent::TextStreamOpened { reader, topic, .. } = event else { + continue; + }; + assert_eq!(topic, "some-topic"); + let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?; + assert_eq!(reader.read_all().await?, expected); + break; + } + Ok(()) + }; + + timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??; + Ok(()) +} + +/// End-to-end round-trip of a large in-memory byte payload, validating the v2 byte-stream path. +#[cfg(feature = "__lk-e2e-test")] +#[tokio::test] +async fn test_send_large_bytes() -> Result<()> { + let mut rooms = test_rooms(2).await?; + let (sending_room, _) = rooms.pop().unwrap(); + let (_, mut receiving_event_rx) = rooms.pop().unwrap(); + + let payload: Vec = (0..50_000u32).map(|i| (i % 251) as u8).collect(); + let expected = payload.clone(); + + let send = async move { + let options = StreamByteOptions { topic: "some-topic".into(), ..Default::default() }; + sending_room.local_participant().send_bytes(&payload, options).await?; + Ok(()) + }; + let receive = async move { + while let Some(event) = receiving_event_rx.recv().await { + let RoomEvent::ByteStreamOpened { reader, topic, .. } = event else { + continue; + }; + assert_eq!(topic, "some-topic"); + let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?; + assert_eq!(reader.read_all().await?, expected); + break; + } + Ok(()) + }; + + timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??; + Ok(()) +} + #[cfg(feature = "__lk-e2e-test")] #[tokio::test] async fn test_send_text() -> Result<()> { From 87b96c7a30c03a4edf6a3e2daee486a66a655495 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 26 Jun 2026 13:18:41 -0400 Subject: [PATCH 02/37] feat: add ability to render data streams in wgpu_room example app --- examples/wgpu_room/src/app.rs | 29 +++++++++++++++++++++++++++++ examples/wgpu_room/src/main.rs | 1 + 2 files changed, 30 insertions(+) diff --git a/examples/wgpu_room/src/app.rs b/examples/wgpu_room/src/app.rs index 65c03ab3a..d21986811 100644 --- a/examples/wgpu_room/src/app.rs +++ b/examples/wgpu_room/src/app.rs @@ -1,4 +1,5 @@ use crate::{ + data_streams_ui::DataStreamsUiState, data_track::{LocalDataTrackTile, RemoteDataTrackTile, MAX_VALUE, TIME_WINDOW}, rpc_ui::RpcUiState, service::{AsyncCmd, LkService, UiCmd}, @@ -25,6 +26,7 @@ struct AppState { enum RightTab { Participants, Rpc, + DataStreams, } pub struct LkApp { @@ -38,6 +40,7 @@ pub struct LkApp { render_state: egui_wgpu::RenderState, service: LkService, rpc_ui: RpcUiState, + data_streams_ui: DataStreamsUiState, right_tab: RightTab, } @@ -74,6 +77,7 @@ impl LkApp { connection_failure: None, render_state: cc.wgpu_render_state.clone().unwrap(), rpc_ui: RpcUiState::default(), + data_streams_ui: DataStreamsUiState::default(), right_tab: RightTab::Participants, } } @@ -130,11 +134,28 @@ impl LkApp { self.remote_data_tracks .push(RemoteDataTrackTile::new(self.async_runtime.handle(), track)); } + RoomEvent::TextStreamOpened { reader, topic, participant_identity } => { + self.data_streams_ui.on_text_stream( + reader, + topic, + participant_identity, + &self.service, + ); + } + RoomEvent::ByteStreamOpened { reader, topic, participant_identity } => { + self.data_streams_ui.on_byte_stream( + reader, + topic, + participant_identity, + &self.service, + ); + } RoomEvent::Disconnected { reason: _ } => { self.video_renderers.clear(); self.local_data_tracks.clear(); self.remote_data_tracks.clear(); self.rpc_ui.on_disconnect(); + self.data_streams_ui.on_disconnect(); } _ => {} } @@ -264,6 +285,7 @@ impl LkApp { ui.horizontal(|ui| { ui.selectable_value(&mut self.right_tab, RightTab::Participants, "Participants"); ui.selectable_value(&mut self.right_tab, RightTab::Rpc, "RPC"); + ui.selectable_value(&mut self.right_tab, RightTab::DataStreams, "Data Streams"); }); ui.separator(); @@ -281,6 +303,13 @@ impl LkApp { rpc_ui.show(ui, service, &room); }); } + RightTab::DataStreams => { + let service = &self.service; + let data_streams_ui = &mut self.data_streams_ui; + egui::ScrollArea::vertical().show(ui, |ui| { + data_streams_ui.show(ui, service, &room); + }); + } } } diff --git a/examples/wgpu_room/src/main.rs b/examples/wgpu_room/src/main.rs index 2d6e8c60e..cd8c3a46f 100644 --- a/examples/wgpu_room/src/main.rs +++ b/examples/wgpu_room/src/main.rs @@ -5,6 +5,7 @@ use std::thread; use std::time::Duration; mod app; +mod data_streams_ui; mod data_track; mod logo_track; mod rpc_ui; From ca896ffc1e13ed0c296a21454536a6cfa7a4de42 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 26 Jun 2026 13:38:38 -0400 Subject: [PATCH 03/37] feat: render participant capabilities and client protocol versions --- examples/wgpu_room/src/app.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/wgpu_room/src/app.rs b/examples/wgpu_room/src/app.rs index d21986811..885db4980 100644 --- a/examples/wgpu_room/src/app.rs +++ b/examples/wgpu_room/src/app.rs @@ -329,6 +329,15 @@ impl LkApp { sorted_tracks.sort_by(|a, b| a.as_str().cmp(b.as_str())); ui.monospace(&participant.identity().0); + ui.label(format!("Client protocol: {}", participant.client_protocol())); + let caps = participant.capabilities(); + let caps_str = if caps.is_empty() { + "(none)".to_string() + } else { + caps.iter().map(|c| format!("{:?}", c)).collect::>().join(", ") + }; + ui.label(format!("Capabilities: {}", caps_str)); + for tsid in sorted_tracks { let publication = tracks.get(&tsid).unwrap().clone(); From 833b6ee1510cc4df3be35d37fee4504c6ef9f4e2 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Fri, 26 Jun 2026 16:47:11 -0400 Subject: [PATCH 04/37] fix: Create data_streams_v2.md --- .changeset/data_streams_v2.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/data_streams_v2.md diff --git a/.changeset/data_streams_v2.md b/.changeset/data_streams_v2.md new file mode 100644 index 000000000..48e58599d --- /dev/null +++ b/.changeset/data_streams_v2.md @@ -0,0 +1,10 @@ +--- +livekit: patch +livekit-api: patch +livekit-datatrack: patch +livekit-ffi: patch +livekit-protocol: patch +livekit-uniffi: patch +--- + +Add data streams v2 - #1192 (@1egoman) From 46a0ac0e23d05762c6f3263f30d64141f8af6b63 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 12:43:04 -0400 Subject: [PATCH 05/37] fix: remove ClientCapability from prelude --- livekit/src/prelude.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index bef9401af..0e0458d3e 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -22,7 +22,7 @@ pub use crate::{ }, id::*, participant::{ - ClientCapability, ConnectionQuality, DisconnectReason, LocalParticipant, Participant, + ConnectionQuality, DisconnectReason, LocalParticipant, Participant, PerformRpcData, RemoteParticipant, RpcError, RpcErrorCode, RpcInvocationData, }, publication::{LocalTrackPublication, RemoteTrackPublication, TrackPublication}, From c92a56feff46bb4aae726f0dcb8b61da7d2b614a Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 12:46:45 -0400 Subject: [PATCH 06/37] feat: allow data stream compress option to be toggled via livekit-ffi --- livekit-ffi/protocol/data_stream.proto | 3 ++- livekit-ffi/src/conversion/data_stream.rs | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/livekit-ffi/protocol/data_stream.proto b/livekit-ffi/protocol/data_stream.proto index 3f9fd590f..83c2ab5be 100644 --- a/livekit-ffi/protocol/data_stream.proto +++ b/livekit-ffi/protocol/data_stream.proto @@ -371,7 +371,7 @@ message StreamTextOptions { optional string reply_to_stream_id = 7; repeated string attached_stream_ids = 8; optional bool generated = 9; - + optional bool compress = 10; } message StreamByteOptions { required string topic = 1; @@ -381,6 +381,7 @@ message StreamByteOptions { optional string name = 5; optional string mime_type = 6; optional uint64 total_length = 7; + optional bool compress = 8; } // Error pertaining to a stream. diff --git a/livekit-ffi/src/conversion/data_stream.rs b/livekit-ffi/src/conversion/data_stream.rs index c47fe7423..140bc74e4 100644 --- a/livekit-ffi/src/conversion/data_stream.rs +++ b/livekit-ffi/src/conversion/data_stream.rs @@ -72,8 +72,7 @@ impl From for StreamTextOptions { reply_to_stream_id: options.reply_to_stream_id, attached_stream_ids: options.attached_stream_ids, generated: options.generated, - // Compression opt-out is not yet exposed over FFI; default to on. - compress: None, + compress: options.compress, } } } @@ -92,8 +91,7 @@ impl From for StreamByteOptions { name: options.name, mime_type: options.mime_type, total_length: options.total_length, - // Compression opt-out is not yet exposed over FFI; default to on. - compress: None, + compress: options.compress, } } } From 7dcdde130a46e370693099cdfc9ff3d2bace0a77 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 12:47:42 -0400 Subject: [PATCH 07/37] fix: run cargo fmt --- livekit/src/prelude.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livekit/src/prelude.rs b/livekit/src/prelude.rs index 0e0458d3e..374558731 100644 --- a/livekit/src/prelude.rs +++ b/livekit/src/prelude.rs @@ -22,8 +22,8 @@ pub use crate::{ }, id::*, participant::{ - ConnectionQuality, DisconnectReason, LocalParticipant, Participant, - PerformRpcData, RemoteParticipant, RpcError, RpcErrorCode, RpcInvocationData, + ConnectionQuality, DisconnectReason, LocalParticipant, Participant, PerformRpcData, + RemoteParticipant, RpcError, RpcErrorCode, RpcInvocationData, }, publication::{LocalTrackPublication, RemoteTrackPublication, TrackPublication}, track::{ From 5952b792c5ce7435ce46a0fe83755759cc8cba95 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:48:28 +0000 Subject: [PATCH 08/37] generated protobuf --- livekit-ffi-node-bindings/proto/data_stream_pb.d.ts | 10 ++++++++++ livekit-ffi-node-bindings/proto/data_stream_pb.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/livekit-ffi-node-bindings/proto/data_stream_pb.d.ts b/livekit-ffi-node-bindings/proto/data_stream_pb.d.ts index 6c7628f37..a800395d6 100644 --- a/livekit-ffi-node-bindings/proto/data_stream_pb.d.ts +++ b/livekit-ffi-node-bindings/proto/data_stream_pb.d.ts @@ -1841,6 +1841,11 @@ export declare class StreamTextOptions extends Message { */ generated?: boolean; + /** + * @generated from field: optional bool compress = 10; + */ + compress?: boolean; + constructor(data?: PartialMessage); static readonly runtime: typeof proto2; @@ -1895,6 +1900,11 @@ export declare class StreamByteOptions extends Message { */ totalLength?: bigint; + /** + * @generated from field: optional bool compress = 8; + */ + compress?: boolean; + constructor(data?: PartialMessage); static readonly runtime: typeof proto2; diff --git a/livekit-ffi-node-bindings/proto/data_stream_pb.js b/livekit-ffi-node-bindings/proto/data_stream_pb.js index cd503c137..6b45dcaad 100644 --- a/livekit-ffi-node-bindings/proto/data_stream_pb.js +++ b/livekit-ffi-node-bindings/proto/data_stream_pb.js @@ -674,6 +674,7 @@ const StreamTextOptions = /*@__PURE__*/ proto2.makeMessageType( { no: 7, name: "reply_to_stream_id", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, { no: 8, name: "attached_stream_ids", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, { no: 9, name: "generated", kind: "scalar", T: 8 /* ScalarType.BOOL */, opt: true }, + { no: 10, name: "compress", kind: "scalar", T: 8 /* ScalarType.BOOL */, opt: true }, ], ); @@ -690,6 +691,7 @@ const StreamByteOptions = /*@__PURE__*/ proto2.makeMessageType( { no: 5, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, { no: 6, name: "mime_type", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true }, { no: 7, name: "total_length", kind: "scalar", T: 4 /* ScalarType.UINT64 */, opt: true }, + { no: 8, name: "compress", kind: "scalar", T: 8 /* ScalarType.BOOL */, opt: true }, ], ); From 3ec690af1616d0d4d5e5700710a5170b284d9770 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 13:00:12 -0400 Subject: [PATCH 09/37] fix: address toc-tou issue with decompressor state --- livekit/src/room/data_stream/incoming.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index 339f4f459..b1fc5f525 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -417,7 +417,7 @@ impl IncomingStreamManager { return; } - if descriptor.decompressor.is_some() { + if let Some(decompressor) = &mut descriptor.decompressor { // --- Compressed stream: feed chunks through one stateful decompressor. --- // Duplicate index (reconnect replay): drop with a warning. if let Some(last) = descriptor.last_chunk_index { @@ -441,12 +441,11 @@ impl IncomingStreamManager { let is_text = descriptor.is_text; // Confine the decompressor borrow so we can re-borrow `inner` afterwards. let result: StreamResult<(u64, Bytes)> = { - let state = descriptor.decompressor.as_mut().unwrap(); - match state.run(&chunk.content) { + match decompressor.run(&chunk.content) { Ok(decompressed) => { let produced = decompressed.len() as u64; let yielded = if is_text { - state.reframe_text(decompressed) + decompressor.reframe_text(decompressed) } else { Bytes::from(decompressed) }; From 0d93e09c5af118aedfdb6ffce855cc536b751407 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 13:31:13 -0400 Subject: [PATCH 10/37] fix: cleanup data stream v2 test fixture initialization --- livekit/src/room/data_stream/outgoing.rs | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index 6853b9b2f..3ebba7193 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -702,9 +702,7 @@ static BYTE_DEFAULT_NAME: &str = "unknown"; mod tests { use super::*; use std::sync::Mutex as StdMutex; - - const V2: i32 = CLIENT_PROTOCOL_DATA_STREAM_V2; - const DEFLATE: ClientCapability = ClientCapability::CompressionDeflateRaw; + use livekit_api::signal_client::{CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT}; // --- Fake recipient registry --------------------------------------------------------- @@ -736,24 +734,26 @@ mod tests { } fn pre_v2_room() -> FakeRegistry { - FakeRegistry::new().add("alice", 0, &[]).add("bob", 0, &[]).add("jim", 1, &[]) + FakeRegistry::new() + .add("alice", CLIENT_PROTOCOL_DEFAULT, &[]) + .add("bob", CLIENT_PROTOCOL_DEFAULT, &[]) + .add("jim", CLIENT_PROTOCOL_DATA_STREAM_RPC, &[]) } fn all_v2_room() -> FakeRegistry { - FakeRegistry::new().add("alice", V2, &[DEFLATE]).add("bob", V2, &[DEFLATE]).add( - "noCompression", - V2, - &[], - ) + FakeRegistry::new() + .add("alice", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw]) + .add("bob", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw]) + .add("noCompression", CLIENT_PROTOCOL_DATA_STREAM_V2, &[]) } fn mixed_room() -> FakeRegistry { FakeRegistry::new() - .add("alice", 0, &[]) - .add("bob", V2, &[DEFLATE]) - .add("jim", V2, &[DEFLATE]) - .add("mallory", 1, &[]) - .add("noCompression", V2, &[]) + .add("alice", CLIENT_PROTOCOL_DEFAULT, &[]) + .add("bob", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw]) + .add("jim", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw]) + .add("mallory", CLIENT_PROTOCOL_DEFAULT, &[]) + .add("noCompression", CLIENT_PROTOCOL_DATA_STREAM_V2, &[]) } // --- Capture harness ----------------------------------------------------------------- From 7162dbc84267b9417ca48549f0b3bbb32aa0e7f6 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 13:31:47 -0400 Subject: [PATCH 11/37] feat: make client capabilities proto deserialization use TryFrom --- livekit/src/room/mod.rs | 12 +++++++++--- livekit/src/room/participant/mod.rs | 22 +++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index b7e38b181..fdabad527 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -577,7 +577,7 @@ impl Room { e2ee_manager.encryption_type(), pi.permission, pi.client_protocol, - pi.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(), + pi.capabilities.iter().filter_map(|&c| ClientCapability::try_from(c).ok()).collect(), ); let dispatcher = Dispatcher::::default(); @@ -760,7 +760,10 @@ impl Room { pi.joined_at_ms, pi.permission, pi.client_protocol, - pi.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(), + pi.capabilities + .iter() + .filter_map(|&c| ClientCapability::try_from(c).ok()) + .collect(), ) }; participant.update_info(pi.clone()); @@ -1197,7 +1200,10 @@ impl RoomSession { pi.joined_at_ms, pi.permission, pi.client_protocol, - pi.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(), + pi.capabilities + .iter() + .filter_map(|&c| ClientCapability::try_from(c).ok()) + .collect(), ) }; diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index 1ff2f6b58..ef049845f 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -90,21 +90,24 @@ pub enum DisconnectReason { /// Stored typed rather than as the raw protobuf `i32` so the public accessor doesn't leak /// protobuf types, while `Unknown` preserves values this SDK build doesn't recognize. #[derive(Debug, Clone, Copy, Eq, PartialEq)] +#[non_exhaustive] pub enum ClientCapability { + Unused, PacketTrailer, CompressionDeflateRaw, - Unknown(i32), } -impl From for ClientCapability { - fn from(value: i32) -> Self { +impl TryFrom for ClientCapability { + type Error = &'static str; + + fn try_from(value: i32) -> Result { match proto::client_info::Capability::try_from(value) { - Ok(proto::client_info::Capability::CapPacketTrailer) => Self::PacketTrailer, + Ok(proto::client_info::Capability::CapPacketTrailer) => Ok(Self::PacketTrailer), Ok(proto::client_info::Capability::CapCompressionDeflateRaw) => { - Self::CompressionDeflateRaw + Ok(Self::CompressionDeflateRaw) } - // `CapUnused` and any value not recognized by this build. - _ => Self::Unknown(value), + Ok(proto::client_info::Capability::CapUnused) => Ok(Self::Unused), + Err(_) => Err("unknown client capability"), } } } @@ -112,13 +115,13 @@ impl From for ClientCapability { impl From for i32 { fn from(value: ClientCapability) -> Self { match value { + ClientCapability::Unused => proto::client_info::Capability::CapUnused as i32, ClientCapability::PacketTrailer => { proto::client_info::Capability::CapPacketTrailer as i32 } ClientCapability::CompressionDeflateRaw => { proto::client_info::Capability::CapCompressionDeflateRaw as i32 } - ClientCapability::Unknown(value) => value, } } } @@ -310,7 +313,8 @@ pub(super) fn update_info( } info.client_protocol = new_info.client_protocol; - info.capabilities = new_info.capabilities.iter().map(|&c| ClientCapability::from(c)).collect(); + info.capabilities = + new_info.capabilities.iter().filter_map(|&c| ClientCapability::try_from(c).ok()).collect(); } pub(super) fn set_speaking( From 10c34c7d71a44bf18c84d92f1d6052d16b89642c Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 13:32:40 -0400 Subject: [PATCH 12/37] fix: run cargo fmt --- livekit/src/room/data_stream/outgoing.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index 3ebba7193..bbf7e1f77 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -701,8 +701,8 @@ static BYTE_DEFAULT_NAME: &str = "unknown"; #[cfg(test)] mod tests { use super::*; - use std::sync::Mutex as StdMutex; use livekit_api::signal_client::{CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT}; + use std::sync::Mutex as StdMutex; // --- Fake recipient registry --------------------------------------------------------- @@ -742,7 +742,11 @@ mod tests { fn all_v2_room() -> FakeRegistry { FakeRegistry::new() - .add("alice", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw]) + .add( + "alice", + CLIENT_PROTOCOL_DATA_STREAM_V2, + &[ClientCapability::CompressionDeflateRaw], + ) .add("bob", CLIENT_PROTOCOL_DATA_STREAM_V2, &[ClientCapability::CompressionDeflateRaw]) .add("noCompression", CLIENT_PROTOCOL_DATA_STREAM_V2, &[]) } From ae7c6d155bfe6cd8ab6c932f3ff1710bc0538702 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 13:42:10 -0400 Subject: [PATCH 13/37] fix: remove dead code --- livekit/src/room/participant/remote_participant.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/livekit/src/room/participant/remote_participant.rs b/livekit/src/room/participant/remote_participant.rs index 618da0cca..6909cdc0d 100644 --- a/livekit/src/room/participant/remote_participant.rs +++ b/livekit/src/room/participant/remote_participant.rs @@ -584,11 +584,6 @@ impl RemoteParticipant { self.inner.info.read().capabilities.clone() } - /// Whether this remote participant can decompress deflate-raw data streams. - pub(crate) fn supports_compression(&self) -> bool { - self.inner.info.read().capabilities.contains(&ClientCapability::CompressionDeflateRaw) - } - pub fn is_encrypted(&self) -> bool { *self.inner.is_encrypted.read() } From d537a7f1938b28ce24421a5793c09690a558fc59 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 13:44:02 -0400 Subject: [PATCH 14/37] fix: add missing data_streams_ui.rs file --- examples/wgpu_room/src/data_streams_ui.rs | 511 ++++++++++++++++++++++ 1 file changed, 511 insertions(+) create mode 100644 examples/wgpu_room/src/data_streams_ui.rs diff --git a/examples/wgpu_room/src/data_streams_ui.rs b/examples/wgpu_room/src/data_streams_ui.rs new file mode 100644 index 000000000..4693d1798 --- /dev/null +++ b/examples/wgpu_room/src/data_streams_ui.rs @@ -0,0 +1,511 @@ +use crate::service::LkService; +use egui::Color32; +use livekit::prelude::*; +use livekit::{ + ByteStreamReader, StreamByteOptions, StreamReader, StreamTextOptions, TakeCell, + TextStreamReader, +}; +use parking_lot::Mutex; +use std::collections::{BTreeMap, VecDeque}; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +const MAX_RECEIVED: usize = 100; +const PREVIEW_CHARS: usize = 256; +const PREVIEW_BYTES: usize = 64; + +/// Whether a stream carries text or raw bytes. Used for both sending and subscribing. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum StreamKind { + Text, + Bytes, +} + +impl StreamKind { + fn label(self) -> &'static str { + match self { + StreamKind::Text => "text", + StreamKind::Bytes => "bytes", + } + } +} + +/// Outcome of the most recent send, shared with the spawned send task. +#[derive(Clone)] +enum SendState { + Idle, + Sending, + Ok(String), + Err(String), +} + +/// A single received stream, rendered as a card. +struct ReceivedStream { + n: u64, + sender: String, + received_at: SystemTime, + size: usize, + compressed: bool, + inline: bool, + preview: String, +} + +/// Accumulates streams received for one (topic, kind) subscription. +struct TopicEntry { + topic: String, + kind: StreamKind, + received: VecDeque, + count: u64, +} + +pub struct DataStreamsUiState { + // Send section + send_kind: StreamKind, + send_topic: String, + send_destination: Option, + send_content: String, + send_hex: bool, + send_state: Arc>, + + // Subscribe section + sub_topic: String, + sub_kind: StreamKind, + sub_error: Option, + subscriptions: BTreeMap<(String, StreamKind), Arc>>, +} + +impl Default for DataStreamsUiState { + fn default() -> Self { + Self { + send_kind: StreamKind::Text, + send_topic: String::new(), + send_destination: None, + send_content: String::new(), + send_hex: false, + send_state: Arc::new(Mutex::new(SendState::Idle)), + sub_topic: String::new(), + sub_kind: StreamKind::Text, + sub_error: None, + subscriptions: BTreeMap::new(), + } + } +} + +impl DataStreamsUiState { + pub fn on_disconnect(&mut self) { + // Subscriptions are pure UI filters (no room registration), so we keep them but clear + // the streams received during the previous session. + for entry in self.subscriptions.values() { + let mut g = entry.lock(); + g.received.clear(); + g.count = 0; + } + *self.send_state.lock() = SendState::Idle; + self.send_destination = None; + } + + /// Routes an incoming text stream to a matching subscription (if any), reading it in the + /// background. Unmatched streams are dropped (the reader is never taken). + pub fn on_text_stream( + &mut self, + reader: TakeCell, + topic: String, + identity: ParticipantIdentity, + service: &LkService, + ) { + let Some(entry) = self.subscriptions.get(&(topic, StreamKind::Text)).cloned() else { + return; + }; + let Some(reader) = reader.take() else { + return; + }; + service.runtime().spawn(async move { + let compressed = reader.info().compressed; + let inline = reader.info().inline; + let (size, preview) = match reader.read_all().await { + Ok(text) => (text.as_bytes().len(), truncate_chars(&text, PREVIEW_CHARS)), + Err(e) => (0, format!("", e)), + }; + push_received(&entry, identity.as_str(), size, compressed, inline, preview); + }); + } + + /// Routes an incoming byte stream to a matching subscription (if any). + pub fn on_byte_stream( + &mut self, + reader: TakeCell, + topic: String, + identity: ParticipantIdentity, + service: &LkService, + ) { + let Some(entry) = self.subscriptions.get(&(topic, StreamKind::Bytes)).cloned() else { + return; + }; + let Some(reader) = reader.take() else { + return; + }; + service.runtime().spawn(async move { + let compressed = reader.info().compressed; + let inline = reader.info().inline; + let (size, preview) = match reader.read_all().await { + Ok(data) => (data.len(), bytes_preview(data.as_ref())), + Err(e) => (0, format!("", e)), + }; + push_received(&entry, identity.as_str(), size, compressed, inline, preview); + }); + } + + pub fn show(&mut self, ui: &mut egui::Ui, service: &LkService, room: &Arc) { + self.show_send(ui, service, room); + ui.add_space(8.0); + ui.separator(); + self.show_subscribe(ui); + self.show_subscription_cards(ui); + } + + fn show_send(&mut self, ui: &mut egui::Ui, service: &LkService, room: &Arc) { + ui.label(egui::RichText::new("Send Data Stream").strong()); + + ui.horizontal(|ui| { + ui.label("Kind:"); + ui.radio_value(&mut self.send_kind, StreamKind::Text, "Text"); + ui.radio_value(&mut self.send_kind, StreamKind::Bytes, "Bytes"); + }); + + ui.horizontal(|ui| { + ui.label("Topic:"); + ui.add(egui::TextEdit::singleline(&mut self.send_topic).desired_width(f32::INFINITY)); + }); + + // Destination picker: broadcast (None) or a specific remote participant. + let participants = room.remote_participants(); + let mut idents: Vec = participants.keys().cloned().collect(); + idents.sort_by(|a, b| a.as_str().cmp(b.as_str())); + if let Some(sel) = self.send_destination.as_ref() { + if !participants.contains_key(sel) { + self.send_destination = None; + } + } + ui.horizontal(|ui| { + ui.label("To:"); + let combo_label = self + .send_destination + .as_ref() + .map(|i| i.as_str().to_string()) + .unwrap_or_else(|| "Everyone (broadcast)".to_string()); + egui::ComboBox::from_id_salt("ds_dest_combo").selected_text(combo_label).show_ui( + ui, + |ui| { + ui.selectable_value(&mut self.send_destination, None, "Everyone (broadcast)"); + for ident in &idents { + ui.selectable_value( + &mut self.send_destination, + Some(ident.clone()), + ident.as_str(), + ); + } + }, + ); + }); + + ui.horizontal(|ui| { + ui.label("Content:"); + if self.send_kind == StreamKind::Text { + if ui.small_button("Hello").clicked() { + self.send_content = "Hello world".to_string(); + } + if ui.small_button("20k").clicked() { + self.send_content = "X".repeat(20_000); + } + } else { + ui.checkbox(&mut self.send_hex, "Hex"); + } + }); + let max_h = ui.text_style_height(&egui::TextStyle::Body) * 5.0 + 8.0; + egui::ScrollArea::vertical().id_salt("ds_send_content_scroll").max_height(max_h).show( + ui, + |ui| { + ui.add( + egui::TextEdit::multiline(&mut self.send_content) + .desired_rows(2) + .desired_width(f32::INFINITY), + ); + }, + ); + + let sending = matches!(&*self.send_state.lock(), SendState::Sending); + let can_send = !sending && !self.send_topic.trim().is_empty(); + + ui.horizontal(|ui| { + ui.add_enabled_ui(can_send, |ui| { + if ui.button("Send").clicked() { + self.dispatch_send(service, room); + } + }); + if sending { + ui.spinner(); + } + }); + + match &*self.send_state.lock() { + SendState::Sending => { + ui.colored_label(Color32::GRAY, "Sending..."); + } + SendState::Ok(s) => { + ui.colored_label(Color32::LIGHT_GREEN, format!("OK: {}", s)); + } + SendState::Err(e) => { + ui.colored_label(Color32::LIGHT_RED, format!("Error: {}", e)); + } + SendState::Idle => {} + } + } + + fn dispatch_send(&mut self, service: &LkService, room: &Arc) { + let topic = self.send_topic.trim().to_string(); + let destination_identities = + self.send_destination.as_ref().map(|i| vec![i.clone()]).unwrap_or_default(); + let local = room.local_participant(); + let state = self.send_state.clone(); + + match self.send_kind { + StreamKind::Text => { + let text = self.send_content.clone(); + let options = + StreamTextOptions { topic, destination_identities, ..Default::default() }; + *state.lock() = SendState::Sending; + service.runtime().spawn(async move { + let result = local.send_text(&text, options).await; + *state.lock() = match result { + Ok(info) => SendState::Ok(format!("stream {}", short_id(&info.id))), + Err(e) => SendState::Err(e.to_string()), + }; + }); + } + StreamKind::Bytes => { + let bytes = if self.send_hex { + match parse_hex(&self.send_content) { + Ok(b) => b, + Err(e) => { + *state.lock() = SendState::Err(format!("invalid hex: {}", e)); + return; + } + } + } else { + self.send_content.as_bytes().to_vec() + }; + let options = + StreamByteOptions { topic, destination_identities, ..Default::default() }; + *state.lock() = SendState::Sending; + service.runtime().spawn(async move { + let result = local.send_bytes(bytes, options).await; + *state.lock() = match result { + Ok(info) => SendState::Ok(format!("stream {}", short_id(&info.id))), + Err(e) => SendState::Err(e.to_string()), + }; + }); + } + } + } + + fn show_subscribe(&mut self, ui: &mut egui::Ui) { + ui.label(egui::RichText::new("Subscriptions").strong()); + + let mut do_add = false; + ui.horizontal(|ui| { + ui.label("Topic:"); + let resp = ui.add(egui::TextEdit::singleline(&mut self.sub_topic).desired_width(120.0)); + if resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + do_add = true; + } + ui.radio_value(&mut self.sub_kind, StreamKind::Text, "Text"); + ui.radio_value(&mut self.sub_kind, StreamKind::Bytes, "Bytes"); + if ui.button("Add").clicked() { + do_add = true; + } + }); + + if do_add { + self.sub_error = None; + let topic = self.sub_topic.trim().to_string(); + let key = (topic.clone(), self.sub_kind); + if topic.is_empty() { + self.sub_error = Some("Topic is empty".to_string()); + } else if self.subscriptions.contains_key(&key) { + self.sub_error = + Some(format!("Already subscribed to '{}' ({})", topic, self.sub_kind.label())); + } else { + self.subscriptions.insert( + key, + Arc::new(Mutex::new(TopicEntry { + topic, + kind: self.sub_kind, + received: VecDeque::new(), + count: 0, + })), + ); + self.sub_topic.clear(); + } + } + + if let Some(err) = &self.sub_error { + ui.colored_label(Color32::LIGHT_RED, err); + } + } + + fn show_subscription_cards(&mut self, ui: &mut egui::Ui) { + let keys: Vec<(String, StreamKind)> = self.subscriptions.keys().cloned().collect(); + let mut to_remove: Option<(String, StreamKind)> = None; + + for key in keys { + let entry = self.subscriptions.get(&key).unwrap().clone(); + ui.add_space(6.0); + egui::Frame::group(ui.style()).show(ui, |ui| { + let guard = entry.lock(); + ui.horizontal(|ui| { + ui.monospace(egui::RichText::new(&guard.topic).strong()); + ui.label( + egui::RichText::new(format!("[{}]", guard.kind.label())) + .small() + .color(Color32::GRAY), + ); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Remove").clicked() { + to_remove = Some(key.clone()); + } + }); + }); + + ui.label(format!("Received ({})", guard.count)); + if guard.received.is_empty() { + ui.colored_label(Color32::GRAY, "Nothing received yet"); + } else { + let max_h = ui.text_style_height(&egui::TextStyle::Body) * 10.0 + 8.0; + egui::ScrollArea::vertical() + .id_salt(format!("ds_recv_scroll_{}_{}", guard.topic, guard.kind.label())) + .max_height(max_h) + .show(ui, |ui| { + for r in guard.received.iter() { + let meta = format!( + "#{} | {} | {} | {}", + r.n, + r.sender, + format_size(r.size), + format_ts(r.received_at), + ); + ui.add(egui::Label::new( + egui::RichText::new(meta).small().color(Color32::GRAY), + )); + ui.horizontal(|ui| { + flag_label(ui, "inline", r.inline); + flag_label(ui, "compressed", r.compressed); + }); + ui.add(egui::Label::new( + egui::RichText::new(&r.preview).monospace(), + )); + ui.separator(); + } + }); + } + }); + } + + if let Some(key) = to_remove { + self.subscriptions.remove(&key); + } + } +} + +fn push_received( + entry: &Arc>, + sender: &str, + size: usize, + compressed: bool, + inline: bool, + preview: String, +) { + let mut g = entry.lock(); + g.count += 1; + let n = g.count; + g.received.push_back(ReceivedStream { + n, + sender: sender.to_string(), + received_at: SystemTime::now(), + size, + compressed, + inline, + preview, + }); + while g.received.len() > MAX_RECEIVED { + g.received.pop_front(); + } +} + +/// Parses a hex string into bytes, ignoring whitespace, commas and colons. +fn parse_hex(s: &str) -> Result, String> { + let cleaned: String = + s.chars().filter(|c| !c.is_whitespace() && *c != ',' && *c != ':').collect(); + if cleaned.len() % 2 != 0 { + return Err("odd number of hex digits".to_string()); + } + let mut out = Vec::with_capacity(cleaned.len() / 2); + let bytes = cleaned.as_bytes(); + let mut i = 0; + while i < bytes.len() { + let pair = &cleaned[i..i + 2]; + let byte = + u8::from_str_radix(pair, 16).map_err(|_| format!("invalid hex byte '{}'", pair))?; + out.push(byte); + i += 2; + } + Ok(out) +} + +/// Renders `: ✓` (green) or `: ✗` (red) for a boolean v2 flag. +fn flag_label(ui: &mut egui::Ui, name: &str, value: bool) { + let (mark, color) = + if value { ("✓", Color32::LIGHT_GREEN) } else { ("✗", Color32::LIGHT_RED) }; + ui.add(egui::Label::new( + egui::RichText::new(format!("{}: {}", name, mark)).small().color(color), + )); +} + +fn bytes_preview(data: &[u8]) -> String { + let shown = &data[..data.len().min(PREVIEW_BYTES)]; + let hex: String = shown.iter().map(|b| format!("{:02x} ", b)).collect(); + let ellipsis = if data.len() > PREVIEW_BYTES { "..." } else { "" }; + let text = String::from_utf8_lossy(shown); + format!("hex: {}{}\nutf8: {}{}", hex.trim_end(), ellipsis, text, ellipsis) +} + +fn truncate_chars(s: &str, max_chars: usize) -> String { + let mut iter = s.chars(); + let head: String = iter.by_ref().take(max_chars).collect(); + if iter.next().is_some() { + format!("{}...", head) + } else { + head + } +} + +fn short_id(id: &str) -> String { + id.chars().take(8).collect() +} + +fn format_size(bytes: usize) -> String { + if bytes < 1024 { + format!("{}B", bytes) + } else { + format!("{:.2}KB", bytes as f64 / 1024.0) + } +} + +fn format_ts(ts: SystemTime) -> String { + let d = ts.duration_since(UNIX_EPOCH).unwrap_or_default(); + let total = d.as_secs(); + let h = (total / 3600) % 24; + let m = (total / 60) % 60; + let s = total % 60; + let ms = d.subsec_millis(); + format!("{:02}:{:02}:{:02}.{:03}Z", h, m, s, ms) +} From 4c5138fbc1ca27db67c0b2f8d8ae7c866ddbb167 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 14:39:10 -0400 Subject: [PATCH 15/37] fix: bump protocol version to try to fix ci build error --- livekit-protocol/protocol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 357f7686d..672b5c86a 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 357f7686de5bab17e1d3e3f1b8b805dba5a0ff56 +Subproject commit 672b5c86a96b47cb617cb0f3a9c06f73a4660c0a From 8e7509fb6b714558c9d97ad82f59edba05559a5e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:41:24 +0000 Subject: [PATCH 16/37] generated protobuf --- livekit-protocol/src/livekit.rs | 267 ++++- livekit-protocol/src/livekit.serde.rs | 1492 ++++++++++++++++++++++++- 2 files changed, 1697 insertions(+), 62 deletions(-) diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index 1af6a6e0e..ab6b74e52 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -94,6 +94,8 @@ pub struct MetricsRecordingHeader { pub room_name: ::prost::alloc::string::String, #[prost(message, optional, tag="7")] pub room_start_time: ::core::option::Option<::pbjson_types::Timestamp>, + #[prost(string, tag="8")] + pub job_id: ::prost::alloc::string::String, } // // Protocol used to record metrics for a specific session. @@ -636,6 +638,177 @@ pub struct DataTrackInfo { /// Method used for end-to-end encryption (E2EE) on packet payloads. #[prost(enumeration="encryption::Type", tag="4")] pub encryption: i32, + /// Encoding for frame payloads on this track. If unspecified, the track is untyped. + #[prost(message, optional, tag="5")] + pub frame_encoding: ::core::option::Option, + /// ID of the schema used by frames on this track if the track is typed. + #[prost(message, optional, tag="6")] + pub schema: ::core::option::Option, +} +/// Encoding for frame payloads. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackFrameEncoding { + #[prost(oneof="data_track_frame_encoding::Value", tags="1, 2")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `DataTrackFrameEncoding`. +pub mod data_track_frame_encoding { + /// Well-known encoding for frame payloads. + /// + /// Mirrors the well-known message encodings from the MCAP spec: + /// + /// + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum WellKnownFrameEncoding { + Unspecified = 0, + /// ROS 1: must be described by `ROS1_MSG` schema encoding. + Ros1 = 1, + /// CDR: must be described by `ROS2_MSG`, `ROS2_IDL`, or `OMG_IDL` schema encoding. + Cdr = 2, + /// Protocol Buffer: must be described by `PROTOBUF` schema encoding. + Protobuf = 3, + /// FlatBuffer: must be described by `FLATBUFFER` schema encoding. + Flatbuffer = 4, + /// CBOR: self-describing. + Cbor = 5, + /// MessagePack: self-describing. + Msgpack = 6, + /// JSON: self-describing or described by `JSON_SCHEMA` schema encoding. + Json = 7, + } + impl WellKnownFrameEncoding { + /// 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 { + WellKnownFrameEncoding::Unspecified => "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED", + WellKnownFrameEncoding::Ros1 => "WELL_KNOWN_FRAME_ENCODING_ROS1", + WellKnownFrameEncoding::Cdr => "WELL_KNOWN_FRAME_ENCODING_CDR", + WellKnownFrameEncoding::Protobuf => "WELL_KNOWN_FRAME_ENCODING_PROTOBUF", + WellKnownFrameEncoding::Flatbuffer => "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER", + WellKnownFrameEncoding::Cbor => "WELL_KNOWN_FRAME_ENCODING_CBOR", + WellKnownFrameEncoding::Msgpack => "WELL_KNOWN_FRAME_ENCODING_MSGPACK", + WellKnownFrameEncoding::Json => "WELL_KNOWN_FRAME_ENCODING_JSON", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED" => Some(Self::Unspecified), + "WELL_KNOWN_FRAME_ENCODING_ROS1" => Some(Self::Ros1), + "WELL_KNOWN_FRAME_ENCODING_CDR" => Some(Self::Cdr), + "WELL_KNOWN_FRAME_ENCODING_PROTOBUF" => Some(Self::Protobuf), + "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER" => Some(Self::Flatbuffer), + "WELL_KNOWN_FRAME_ENCODING_CBOR" => Some(Self::Cbor), + "WELL_KNOWN_FRAME_ENCODING_MSGPACK" => Some(Self::Msgpack), + "WELL_KNOWN_FRAME_ENCODING_JSON" => Some(Self::Json), + _ => None, + } + } + } + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(enumeration="WellKnownFrameEncoding", tag="1")] + WellKnown(i32), + /// Identifier of a custom encoding not covered by the well-known cases. + /// This must be non-empty and no longer than 32 characters. + #[prost(string, tag="2")] + Custom(::prost::alloc::string::String), + } +} +/// Encoding for schema definition. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSchemaEncoding { + #[prost(oneof="data_track_schema_encoding::Value", tags="1, 2")] + pub value: ::core::option::Option, +} +/// Nested message and enum types in `DataTrackSchemaEncoding`. +pub mod data_track_schema_encoding { + /// Well-known encoding for schema definition. + /// + /// Mirrors the well-known schema encodings from the MCAP spec: + /// + /// + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] + #[repr(i32)] + pub enum WellKnownSchemaEncoding { + Unspecified = 0, + /// Protocol Buffer IDL: describes `PROTOBUF` frame encoding. + Protobuf = 1, + /// FlatBuffer IDL: describes `FLATBUFFER` frame encoding. + Flatbuffer = 2, + /// ROS 1 Message: describes `ROS1` frame encoding. + Ros1Msg = 3, + /// ROS 2 Message: describes `CDR` frame encoding. + Ros2Msg = 4, + /// ROS 2 IDL: describes `CDR` frame encoding. + Ros2Idl = 5, + /// OMG IDL: describes `CDR` frame encoding. + OmgIdl = 6, + /// JSON Schema: describes `JSON` frame encoding. + JsonSchema = 7, + } + impl WellKnownSchemaEncoding { + /// 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 { + WellKnownSchemaEncoding::Unspecified => "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED", + WellKnownSchemaEncoding::Protobuf => "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF", + WellKnownSchemaEncoding::Flatbuffer => "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER", + WellKnownSchemaEncoding::Ros1Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG", + WellKnownSchemaEncoding::Ros2Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG", + WellKnownSchemaEncoding::Ros2Idl => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL", + WellKnownSchemaEncoding::OmgIdl => "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL", + WellKnownSchemaEncoding::JsonSchema => "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED" => Some(Self::Unspecified), + "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF" => Some(Self::Protobuf), + "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER" => Some(Self::Flatbuffer), + "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG" => Some(Self::Ros1Msg), + "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG" => Some(Self::Ros2Msg), + "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL" => Some(Self::Ros2Idl), + "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL" => Some(Self::OmgIdl), + "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA" => Some(Self::JsonSchema), + _ => None, + } + } + } + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Value { + #[prost(enumeration="WellKnownSchemaEncoding", tag="1")] + WellKnown(i32), + /// Identifier of a custom encoding not covered by the well-known cases. + /// This must be non-empty and no longer than 32 characters. + #[prost(string, tag="2")] + Custom(::prost::alloc::string::String), + } +} +/// Identifier for a data track schema. +/// +/// Schemas with the same name but different encodings are distinct. +/// +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataTrackSchemaId { + /// This must be non-empty and no longer than 256 characters. + #[prost(string, tag="1")] + pub name: ::prost::alloc::string::String, + #[prost(message, optional, tag="2")] + pub encoding: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -653,6 +826,37 @@ pub struct DataTrackSubscriptionOptions { #[prost(uint32, optional, tag="1")] pub target_fps: ::core::option::Option, } +/// Key used to uniquely identify a data blob for storage and retrieval. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataBlobKey { + #[prost(oneof="data_blob_key::Key", tags="1, 2")] + pub key: ::core::option::Option, +} +/// Nested message and enum types in `DataBlobKey`. +pub mod data_blob_key { + #[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Key { + /// Generic string key, blob contains arbitrary data. + #[prost(string, tag="1")] + Generic(::prost::alloc::string::String), + /// Data track schema identifier, blob contains schema definition. + #[prost(message, tag="2")] + SchemaId(super::DataTrackSchemaId), + } +} +/// A blob of data stored in a room identified by a unique key. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct DataBlob { + /// Unique key the data blob is identified by. + #[prost(message, optional, tag="1")] + pub key: ::core::option::Option, + /// Contents of the data blob. This must not exceed 50 KB. + #[prost(bytes="vec", tag="2")] + pub contents: ::prost::alloc::vec::Vec, +} /// provide information about available spatial layers #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3551,7 +3755,7 @@ impl AudioMixing { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalRequest { - #[prost(oneof="signal_request::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21")] + #[prost(oneof="signal_request::Message", tags="1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalRequest`. @@ -3619,12 +3823,18 @@ pub mod signal_request { /// Update subscription state for one or more data tracks #[prost(message, tag="21")] UpdateDataSubscription(super::UpdateDataSubscription), + /// Store a data blob. + #[prost(message, tag="22")] + StoreDataBlobRequest(super::StoreDataBlobRequest), + /// Retrieve a stored data blob. + #[prost(message, tag="23")] + GetDataBlobRequest(super::GetDataBlobRequest), } } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct SignalResponse { - #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29")] + #[prost(oneof="signal_response::Message", tags="1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31")] pub message: ::core::option::Option, } /// Nested message and enum types in `SignalResponse`. @@ -3719,6 +3929,12 @@ pub mod signal_response { /// Sent to data track subscribers to provide mapping from track SIDs to handles. #[prost(message, tag="29")] DataTrackSubscriberHandles(super::DataTrackSubscriberHandles), + /// Sent in response to `StoreDataBlobRequest`. + #[prost(message, tag="30")] + StoreDataBlobResponse(super::StoreDataBlobResponse), + /// Sent in response to `GetDataBlobRequest`. + #[prost(message, tag="31")] + GetDataBlobResponse(super::GetDataBlobResponse), } } #[allow(clippy::derive_partial_eq_without_eq)] @@ -3799,6 +4015,13 @@ pub struct PublishDataTrackRequest { /// Method used for end-to-end encryption (E2EE) on frame payloads. #[prost(enumeration="encryption::Type", tag="3")] pub encryption: i32, + /// Encoding for frame payloads on this track. If unspecified, the track is untyped. + #[prost(message, optional, tag="4")] + pub frame_encoding: ::core::option::Option, + /// ID of the schema used by frames on this track if the track is typed. + /// If set, the associated schema must be stored with `StoreDataBlobRequest`. + #[prost(message, optional, tag="5")] + pub schema: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -3979,6 +4202,43 @@ pub mod update_data_subscription { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct StoreDataBlobRequest { + #[prost(uint32, tag="1")] + pub request_id: u32, + #[prost(message, optional, tag="2")] + pub blob: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StoreDataBlobResponse { + #[prost(uint32, tag="1")] + pub request_id: u32, + /// Unique key the data blob was stored under. + #[prost(message, optional, tag="2")] + pub key: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetDataBlobRequest { + #[prost(uint32, tag="1")] + pub request_id: u32, + /// Identity of the participant who owns the blob. + #[prost(string, tag="2")] + pub participant_identity: ::prost::alloc::string::String, + /// Unique key of the data blob to retrieve. + #[prost(message, optional, tag="3")] + pub key: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetDataBlobResponse { + #[prost(uint32, tag="1")] + pub request_id: u32, + #[prost(message, optional, tag="2")] + pub blob: ::core::option::Option, +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct UpdateTrackSettings { #[prost(string, repeated, tag="1")] pub track_sids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, @@ -4387,6 +4647,7 @@ pub mod request_response { InvalidName = 8, DuplicateHandle = 9, DuplicateName = 10, + InvalidRequest = 11, } impl Reason { /// String value of the enum field names used in the ProtoBuf definition. @@ -4406,6 +4667,7 @@ pub mod request_response { Reason::InvalidName => "INVALID_NAME", Reason::DuplicateHandle => "DUPLICATE_HANDLE", Reason::DuplicateName => "DUPLICATE_NAME", + Reason::InvalidRequest => "INVALID_REQUEST", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -4422,6 +4684,7 @@ pub mod request_response { "INVALID_NAME" => Some(Self::InvalidName), "DUPLICATE_HANDLE" => Some(Self::DuplicateHandle), "DUPLICATE_NAME" => Some(Self::DuplicateName), + "INVALID_REQUEST" => Some(Self::InvalidRequest), _ => None, } } diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index ebd07f129..3ff3c3c0f 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -10527,6 +10527,235 @@ impl<'de> serde::Deserialize<'de> for CreateSipTrunkRequest { deserializer.deserialize_struct("livekit.CreateSIPTrunkRequest", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataBlob { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.key.is_some() { + len += 1; + } + if !self.contents.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataBlob", len)?; + if let Some(v) = self.key.as_ref() { + struct_ser.serialize_field("key", v)?; + } + if !self.contents.is_empty() { + #[allow(clippy::needless_borrow)] + #[allow(clippy::needless_borrows_for_generic_args)] + struct_ser.serialize_field("contents", pbjson::private::base64::encode(&self.contents).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataBlob { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "key", + "contents", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Key, + Contents, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "key" => Ok(GeneratedField::Key), + "contents" => Ok(GeneratedField::Contents), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataBlob; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataBlob") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut key__ = None; + let mut contents__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Key => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("key")); + } + key__ = map_.next_value()?; + } + GeneratedField::Contents => { + if contents__.is_some() { + return Err(serde::de::Error::duplicate_field("contents")); + } + contents__ = + Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0) + ; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataBlob { + key: key__, + contents: contents__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("livekit.DataBlob", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DataBlobKey { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.key.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataBlobKey", len)?; + if let Some(v) = self.key.as_ref() { + match v { + data_blob_key::Key::Generic(v) => { + struct_ser.serialize_field("generic", v)?; + } + data_blob_key::Key::SchemaId(v) => { + struct_ser.serialize_field("schemaId", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataBlobKey { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "generic", + "schema_id", + "schemaId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Generic, + SchemaId, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "generic" => Ok(GeneratedField::Generic), + "schemaId" | "schema_id" => Ok(GeneratedField::SchemaId), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataBlobKey; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataBlobKey") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut key__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Generic => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("generic")); + } + key__ = map_.next_value::<::std::option::Option<_>>()?.map(data_blob_key::Key::Generic); + } + GeneratedField::SchemaId => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("schemaId")); + } + key__ = map_.next_value::<::std::option::Option<_>>()?.map(data_blob_key::Key::SchemaId) +; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataBlobKey { + key: key__, + }) + } + } + deserializer.deserialize_struct("livekit.DataBlobKey", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for DataChannelInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -12703,6 +12932,209 @@ impl<'de> serde::Deserialize<'de> for DataTrackExtensionParticipantSid { deserializer.deserialize_struct("livekit.DataTrackExtensionParticipantSid", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for DataTrackFrameEncoding { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackFrameEncoding", len)?; + if let Some(v) = self.value.as_ref() { + match v { + data_track_frame_encoding::Value::WellKnown(v) => { + let v = data_track_frame_encoding::WellKnownFrameEncoding::try_from(*v) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; + struct_ser.serialize_field("wellKnown", &v)?; + } + data_track_frame_encoding::Value::Custom(v) => { + struct_ser.serialize_field("custom", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackFrameEncoding { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "well_known", + "wellKnown", + "custom", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + WellKnown, + Custom, + __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 { + "wellKnown" | "well_known" => Ok(GeneratedField::WellKnown), + "custom" => Ok(GeneratedField::Custom), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackFrameEncoding; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackFrameEncoding") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::WellKnown => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("wellKnown")); + } + value__ = map_.next_value::<::std::option::Option>()?.map(|x| data_track_frame_encoding::Value::WellKnown(x as i32)); + } + GeneratedField::Custom => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("custom")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(data_track_frame_encoding::Value::Custom); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackFrameEncoding { + value: value__, + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackFrameEncoding", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for data_track_frame_encoding::WellKnownFrameEncoding { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unspecified => "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED", + Self::Ros1 => "WELL_KNOWN_FRAME_ENCODING_ROS1", + Self::Cdr => "WELL_KNOWN_FRAME_ENCODING_CDR", + Self::Protobuf => "WELL_KNOWN_FRAME_ENCODING_PROTOBUF", + Self::Flatbuffer => "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER", + Self::Cbor => "WELL_KNOWN_FRAME_ENCODING_CBOR", + Self::Msgpack => "WELL_KNOWN_FRAME_ENCODING_MSGPACK", + Self::Json => "WELL_KNOWN_FRAME_ENCODING_JSON", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for data_track_frame_encoding::WellKnownFrameEncoding { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED", + "WELL_KNOWN_FRAME_ENCODING_ROS1", + "WELL_KNOWN_FRAME_ENCODING_CDR", + "WELL_KNOWN_FRAME_ENCODING_PROTOBUF", + "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER", + "WELL_KNOWN_FRAME_ENCODING_CBOR", + "WELL_KNOWN_FRAME_ENCODING_MSGPACK", + "WELL_KNOWN_FRAME_ENCODING_JSON", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = data_track_frame_encoding::WellKnownFrameEncoding; + + 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 { + "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Unspecified), + "WELL_KNOWN_FRAME_ENCODING_ROS1" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Ros1), + "WELL_KNOWN_FRAME_ENCODING_CDR" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Cdr), + "WELL_KNOWN_FRAME_ENCODING_PROTOBUF" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Protobuf), + "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Flatbuffer), + "WELL_KNOWN_FRAME_ENCODING_CBOR" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Cbor), + "WELL_KNOWN_FRAME_ENCODING_MSGPACK" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Msgpack), + "WELL_KNOWN_FRAME_ENCODING_JSON" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Json), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} impl serde::Serialize for DataTrackInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -12723,6 +13155,12 @@ impl serde::Serialize for DataTrackInfo { if self.encryption != 0 { len += 1; } + if self.frame_encoding.is_some() { + len += 1; + } + if self.schema.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.DataTrackInfo", len)?; if self.pub_handle != 0 { struct_ser.serialize_field("pubHandle", &self.pub_handle)?; @@ -12738,6 +13176,12 @@ impl serde::Serialize for DataTrackInfo { .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; struct_ser.serialize_field("encryption", &v)?; } + if let Some(v) = self.frame_encoding.as_ref() { + struct_ser.serialize_field("frameEncoding", v)?; + } + if let Some(v) = self.schema.as_ref() { + struct_ser.serialize_field("schema", v)?; + } struct_ser.end() } } @@ -12753,6 +13197,9 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { "sid", "name", "encryption", + "frame_encoding", + "frameEncoding", + "schema", ]; #[allow(clippy::enum_variant_names)] @@ -12761,6 +13208,361 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { Sid, Name, Encryption, + FrameEncoding, + Schema, + __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), + "sid" => Ok(GeneratedField::Sid), + "name" => Ok(GeneratedField::Name), + "encryption" => Ok(GeneratedField::Encryption), + "frameEncoding" | "frame_encoding" => Ok(GeneratedField::FrameEncoding), + "schema" => Ok(GeneratedField::Schema), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackInfo; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackInfo") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pub_handle__ = None; + let mut sid__ = None; + let mut name__ = None; + let mut encryption__ = None; + let mut frame_encoding__ = None; + let mut schema__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PubHandle => { + 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::FrameEncoding => { + if frame_encoding__.is_some() { + return Err(serde::de::Error::duplicate_field("frameEncoding")); + } + frame_encoding__ = map_.next_value()?; + } + GeneratedField::Schema => { + if schema__.is_some() { + return Err(serde::de::Error::duplicate_field("schema")); + } + schema__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackInfo { + pub_handle: pub_handle__.unwrap_or_default(), + sid: sid__.unwrap_or_default(), + name: name__.unwrap_or_default(), + encryption: encryption__.unwrap_or_default(), + frame_encoding: frame_encoding__, + schema: schema__, + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackSchemaEncoding { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.value.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSchemaEncoding", len)?; + if let Some(v) = self.value.as_ref() { + match v { + data_track_schema_encoding::Value::WellKnown(v) => { + let v = data_track_schema_encoding::WellKnownSchemaEncoding::try_from(*v) + .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; + struct_ser.serialize_field("wellKnown", &v)?; + } + data_track_schema_encoding::Value::Custom(v) => { + struct_ser.serialize_field("custom", v)?; + } + } + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSchemaEncoding { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "well_known", + "wellKnown", + "custom", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + WellKnown, + Custom, + __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 { + "wellKnown" | "well_known" => Ok(GeneratedField::WellKnown), + "custom" => Ok(GeneratedField::Custom), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = DataTrackSchemaEncoding; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.DataTrackSchemaEncoding") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut value__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::WellKnown => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("wellKnown")); + } + value__ = map_.next_value::<::std::option::Option>()?.map(|x| data_track_schema_encoding::Value::WellKnown(x as i32)); + } + GeneratedField::Custom => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("custom")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(data_track_schema_encoding::Value::Custom); + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(DataTrackSchemaEncoding { + value: value__, + }) + } + } + deserializer.deserialize_struct("livekit.DataTrackSchemaEncoding", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for data_track_schema_encoding::WellKnownSchemaEncoding { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + let variant = match self { + Self::Unspecified => "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED", + Self::Protobuf => "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF", + Self::Flatbuffer => "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER", + Self::Ros1Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG", + Self::Ros2Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG", + Self::Ros2Idl => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL", + Self::OmgIdl => "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL", + Self::JsonSchema => "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA", + }; + serializer.serialize_str(variant) + } +} +impl<'de> serde::Deserialize<'de> for data_track_schema_encoding::WellKnownSchemaEncoding { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED", + "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF", + "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER", + "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG", + "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG", + "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL", + "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL", + "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA", + ]; + + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = data_track_schema_encoding::WellKnownSchemaEncoding; + + 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 { + "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Unspecified), + "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Protobuf), + "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Flatbuffer), + "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Ros1Msg), + "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Ros2Msg), + "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Ros2Idl), + "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::OmgIdl), + "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::JsonSchema), + _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), + } + } + } + deserializer.deserialize_any(GeneratedVisitor) + } +} +impl serde::Serialize for DataTrackSchemaId { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.name.is_empty() { + len += 1; + } + if self.encoding.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSchemaId", len)?; + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + if let Some(v) = self.encoding.as_ref() { + struct_ser.serialize_field("encoding", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for DataTrackSchemaId { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "name", + "encoding", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Name, + Encoding, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -12783,10 +13585,8 @@ 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), - "encryption" => Ok(GeneratedField::Encryption), + "encoding" => Ok(GeneratedField::Encoding), _ => Ok(GeneratedField::__SkipField__), } } @@ -12796,62 +13596,44 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DataTrackInfo; + type Value = DataTrackSchemaId; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DataTrackInfo") + formatter.write_str("struct livekit.DataTrackSchemaId") } - 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 pub_handle__ = None; - let mut sid__ = None; let mut name__ = None; - let mut encryption__ = None; + let mut encoding__ = 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")); + GeneratedField::Encoding => { + if encoding__.is_some() { + return Err(serde::de::Error::duplicate_field("encoding")); } - encryption__ = Some(map_.next_value::()? as i32); + encoding__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(DataTrackInfo { - pub_handle: pub_handle__.unwrap_or_default(), - sid: sid__.unwrap_or_default(), + Ok(DataTrackSchemaId { name: name__.unwrap_or_default(), - encryption: encryption__.unwrap_or_default(), + encoding: encoding__, }) } } - deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.DataTrackSchemaId", FIELDS, GeneratedVisitor) } } impl serde::Serialize for DataTrackSubscriberHandles { @@ -18455,9 +19237,263 @@ impl<'de> serde::Deserialize<'de> for GcpUpload { E: serde::de::Error, { match value { - "credentials" => Ok(GeneratedField::Credentials), - "bucket" => Ok(GeneratedField::Bucket), - "proxy" => Ok(GeneratedField::Proxy), + "credentials" => Ok(GeneratedField::Credentials), + "bucket" => Ok(GeneratedField::Bucket), + "proxy" => Ok(GeneratedField::Proxy), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GcpUpload; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.GCPUpload") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut credentials__ = None; + let mut bucket__ = None; + let mut proxy__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Credentials => { + if credentials__.is_some() { + return Err(serde::de::Error::duplicate_field("credentials")); + } + credentials__ = Some(map_.next_value()?); + } + GeneratedField::Bucket => { + if bucket__.is_some() { + return Err(serde::de::Error::duplicate_field("bucket")); + } + bucket__ = Some(map_.next_value()?); + } + GeneratedField::Proxy => { + if proxy__.is_some() { + return Err(serde::de::Error::duplicate_field("proxy")); + } + proxy__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(GcpUpload { + credentials: credentials__.unwrap_or_default(), + bucket: bucket__.unwrap_or_default(), + proxy: proxy__, + }) + } + } + deserializer.deserialize_struct("livekit.GCPUpload", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetDataBlobRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.request_id != 0 { + len += 1; + } + if !self.participant_identity.is_empty() { + len += 1; + } + if self.key.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobRequest", len)?; + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } + if !self.participant_identity.is_empty() { + struct_ser.serialize_field("participantIdentity", &self.participant_identity)?; + } + if let Some(v) = self.key.as_ref() { + struct_ser.serialize_field("key", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetDataBlobRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "request_id", + "requestId", + "participant_identity", + "participantIdentity", + "key", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RequestId, + ParticipantIdentity, + Key, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "participantIdentity" | "participant_identity" => Ok(GeneratedField::ParticipantIdentity), + "key" => Ok(GeneratedField::Key), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GetDataBlobRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.GetDataBlobRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut request_id__ = None; + let mut participant_identity__ = None; + let mut key__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::ParticipantIdentity => { + if participant_identity__.is_some() { + return Err(serde::de::Error::duplicate_field("participantIdentity")); + } + participant_identity__ = Some(map_.next_value()?); + } + GeneratedField::Key => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("key")); + } + key__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(GetDataBlobRequest { + request_id: request_id__.unwrap_or_default(), + participant_identity: participant_identity__.unwrap_or_default(), + key: key__, + }) + } + } + deserializer.deserialize_struct("livekit.GetDataBlobRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for GetDataBlobResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.request_id != 0 { + len += 1; + } + if self.blob.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.GetDataBlobResponse", len)?; + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } + if let Some(v) = self.blob.as_ref() { + struct_ser.serialize_field("blob", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for GetDataBlobResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "request_id", + "requestId", + "blob", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RequestId, + Blob, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "blob" => Ok(GeneratedField::Blob), _ => Ok(GeneratedField::__SkipField__), } } @@ -18467,52 +19503,46 @@ impl<'de> serde::Deserialize<'de> for GcpUpload { } struct GeneratedVisitor; impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = GcpUpload; + type Value = GetDataBlobResponse; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.GCPUpload") + formatter.write_str("struct livekit.GetDataBlobResponse") } - fn visit_map(self, mut map_: V) -> std::result::Result + fn visit_map(self, mut map_: V) -> std::result::Result where V: serde::de::MapAccess<'de>, { - let mut credentials__ = None; - let mut bucket__ = None; - let mut proxy__ = None; + let mut request_id__ = None; + let mut blob__ = None; while let Some(k) = map_.next_key()? { match k { - GeneratedField::Credentials => { - if credentials__.is_some() { - return Err(serde::de::Error::duplicate_field("credentials")); - } - credentials__ = Some(map_.next_value()?); - } - GeneratedField::Bucket => { - if bucket__.is_some() { - return Err(serde::de::Error::duplicate_field("bucket")); + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); } - bucket__ = Some(map_.next_value()?); + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; } - GeneratedField::Proxy => { - if proxy__.is_some() { - return Err(serde::de::Error::duplicate_field("proxy")); + GeneratedField::Blob => { + if blob__.is_some() { + return Err(serde::de::Error::duplicate_field("blob")); } - proxy__ = map_.next_value()?; + blob__ = map_.next_value()?; } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } } } - Ok(GcpUpload { - credentials: credentials__.unwrap_or_default(), - bucket: bucket__.unwrap_or_default(), - proxy: proxy__, + Ok(GetDataBlobResponse { + request_id: request_id__.unwrap_or_default(), + blob: blob__, }) } } - deserializer.deserialize_struct("livekit.GCPUpload", FIELDS, GeneratedVisitor) + deserializer.deserialize_struct("livekit.GetDataBlobResponse", FIELDS, GeneratedVisitor) } } impl serde::Serialize for GetSipInboundTrunkRequest { @@ -26099,6 +27129,9 @@ impl serde::Serialize for MetricsRecordingHeader { if self.room_start_time.is_some() { len += 1; } + if !self.job_id.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)?; @@ -26120,6 +27153,9 @@ impl serde::Serialize for MetricsRecordingHeader { if let Some(v) = self.room_start_time.as_ref() { struct_ser.serialize_field("roomStartTime", v)?; } + if !self.job_id.is_empty() { + struct_ser.serialize_field("jobId", &self.job_id)?; + } struct_ser.end() } } @@ -26141,6 +27177,8 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "roomName", "room_start_time", "roomStartTime", + "job_id", + "jobId", ]; #[allow(clippy::enum_variant_names)] @@ -26151,6 +27189,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { RoomTags, RoomName, RoomStartTime, + JobId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -26179,6 +27218,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "roomTags" | "room_tags" => Ok(GeneratedField::RoomTags), "roomName" | "room_name" => Ok(GeneratedField::RoomName), "roomStartTime" | "room_start_time" => Ok(GeneratedField::RoomStartTime), + "jobId" | "job_id" => Ok(GeneratedField::JobId), _ => Ok(GeneratedField::__SkipField__), } } @@ -26204,6 +27244,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { let mut room_tags__ = None; let mut room_name__ = None; let mut room_start_time__ = None; + let mut job_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RoomId => { @@ -26246,6 +27287,12 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { } room_start_time__ = map_.next_value()?; } + GeneratedField::JobId => { + if job_id__.is_some() { + return Err(serde::de::Error::duplicate_field("jobId")); + } + job_id__ = Some(map_.next_value()?); + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -26258,6 +27305,7 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { room_tags: room_tags__.unwrap_or_default(), room_name: room_name__.unwrap_or_default(), room_start_time: room_start_time__, + job_id: job_id__.unwrap_or_default(), }) } } @@ -29782,6 +30830,12 @@ impl serde::Serialize for PublishDataTrackRequest { if self.encryption != 0 { len += 1; } + if self.frame_encoding.is_some() { + len += 1; + } + if self.schema.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackRequest", len)?; if self.pub_handle != 0 { struct_ser.serialize_field("pubHandle", &self.pub_handle)?; @@ -29794,6 +30848,12 @@ impl serde::Serialize for PublishDataTrackRequest { .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; struct_ser.serialize_field("encryption", &v)?; } + if let Some(v) = self.frame_encoding.as_ref() { + struct_ser.serialize_field("frameEncoding", v)?; + } + if let Some(v) = self.schema.as_ref() { + struct_ser.serialize_field("schema", v)?; + } struct_ser.end() } } @@ -29808,6 +30868,9 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { "pubHandle", "name", "encryption", + "frame_encoding", + "frameEncoding", + "schema", ]; #[allow(clippy::enum_variant_names)] @@ -29815,6 +30878,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { PubHandle, Name, Encryption, + FrameEncoding, + Schema, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -29840,6 +30905,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), "name" => Ok(GeneratedField::Name), "encryption" => Ok(GeneratedField::Encryption), + "frameEncoding" | "frame_encoding" => Ok(GeneratedField::FrameEncoding), + "schema" => Ok(GeneratedField::Schema), _ => Ok(GeneratedField::__SkipField__), } } @@ -29862,6 +30929,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { let mut pub_handle__ = None; let mut name__ = None; let mut encryption__ = None; + let mut frame_encoding__ = None; + let mut schema__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PubHandle => { @@ -29884,6 +30953,18 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { } encryption__ = Some(map_.next_value::()? as i32); } + GeneratedField::FrameEncoding => { + if frame_encoding__.is_some() { + return Err(serde::de::Error::duplicate_field("frameEncoding")); + } + frame_encoding__ = map_.next_value()?; + } + GeneratedField::Schema => { + if schema__.is_some() { + return Err(serde::de::Error::duplicate_field("schema")); + } + schema__ = map_.next_value()?; + } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -29893,6 +30974,8 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { pub_handle: pub_handle__.unwrap_or_default(), name: name__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), + frame_encoding: frame_encoding__, + schema: schema__, }) } } @@ -33107,6 +34190,7 @@ impl serde::Serialize for request_response::Reason { Self::InvalidName => "INVALID_NAME", Self::DuplicateHandle => "DUPLICATE_HANDLE", Self::DuplicateName => "DUPLICATE_NAME", + Self::InvalidRequest => "INVALID_REQUEST", }; serializer.serialize_str(variant) } @@ -33129,6 +34213,7 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "INVALID_NAME", "DUPLICATE_HANDLE", "DUPLICATE_NAME", + "INVALID_REQUEST", ]; struct GeneratedVisitor; @@ -33180,6 +34265,7 @@ impl<'de> serde::Deserialize<'de> for request_response::Reason { "INVALID_NAME" => Ok(request_response::Reason::InvalidName), "DUPLICATE_HANDLE" => Ok(request_response::Reason::DuplicateHandle), "DUPLICATE_NAME" => Ok(request_response::Reason::DuplicateName), + "INVALID_REQUEST" => Ok(request_response::Reason::InvalidRequest), _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), } } @@ -43100,6 +44186,12 @@ impl serde::Serialize for SignalRequest { signal_request::Message::UpdateDataSubscription(v) => { struct_ser.serialize_field("updateDataSubscription", v)?; } + signal_request::Message::StoreDataBlobRequest(v) => { + struct_ser.serialize_field("storeDataBlobRequest", v)?; + } + signal_request::Message::GetDataBlobRequest(v) => { + struct_ser.serialize_field("getDataBlobRequest", v)?; + } } } struct_ser.end() @@ -43144,6 +44236,10 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "unpublishDataTrackRequest", "update_data_subscription", "updateDataSubscription", + "store_data_blob_request", + "storeDataBlobRequest", + "get_data_blob_request", + "getDataBlobRequest", ]; #[allow(clippy::enum_variant_names)] @@ -43168,6 +44264,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { PublishDataTrackRequest, UnpublishDataTrackRequest, UpdateDataSubscription, + StoreDataBlobRequest, + GetDataBlobRequest, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -43210,6 +44308,8 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { "publishDataTrackRequest" | "publish_data_track_request" => Ok(GeneratedField::PublishDataTrackRequest), "unpublishDataTrackRequest" | "unpublish_data_track_request" => Ok(GeneratedField::UnpublishDataTrackRequest), "updateDataSubscription" | "update_data_subscription" => Ok(GeneratedField::UpdateDataSubscription), + "storeDataBlobRequest" | "store_data_blob_request" => Ok(GeneratedField::StoreDataBlobRequest), + "getDataBlobRequest" | "get_data_blob_request" => Ok(GeneratedField::GetDataBlobRequest), _ => Ok(GeneratedField::__SkipField__), } } @@ -43369,6 +44469,20 @@ impl<'de> serde::Deserialize<'de> for SignalRequest { return Err(serde::de::Error::duplicate_field("updateDataSubscription")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::UpdateDataSubscription) +; + } + GeneratedField::StoreDataBlobRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("storeDataBlobRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::StoreDataBlobRequest) +; + } + GeneratedField::GetDataBlobRequest => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("getDataBlobRequest")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_request::Message::GetDataBlobRequest) ; } GeneratedField::__SkipField__ => { @@ -43484,6 +44598,12 @@ impl serde::Serialize for SignalResponse { signal_response::Message::DataTrackSubscriberHandles(v) => { struct_ser.serialize_field("dataTrackSubscriberHandles", v)?; } + signal_response::Message::StoreDataBlobResponse(v) => { + struct_ser.serialize_field("storeDataBlobResponse", v)?; + } + signal_response::Message::GetDataBlobResponse(v) => { + struct_ser.serialize_field("getDataBlobResponse", v)?; + } } } struct_ser.end() @@ -43543,6 +44663,10 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "unpublishDataTrackResponse", "data_track_subscriber_handles", "dataTrackSubscriberHandles", + "store_data_blob_response", + "storeDataBlobResponse", + "get_data_blob_response", + "getDataBlobResponse", ]; #[allow(clippy::enum_variant_names)] @@ -43575,6 +44699,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { PublishDataTrackResponse, UnpublishDataTrackResponse, DataTrackSubscriberHandles, + StoreDataBlobResponse, + GetDataBlobResponse, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -43625,6 +44751,8 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { "publishDataTrackResponse" | "publish_data_track_response" => Ok(GeneratedField::PublishDataTrackResponse), "unpublishDataTrackResponse" | "unpublish_data_track_response" => Ok(GeneratedField::UnpublishDataTrackResponse), "dataTrackSubscriberHandles" | "data_track_subscriber_handles" => Ok(GeneratedField::DataTrackSubscriberHandles), + "storeDataBlobResponse" | "store_data_blob_response" => Ok(GeneratedField::StoreDataBlobResponse), + "getDataBlobResponse" | "get_data_blob_response" => Ok(GeneratedField::GetDataBlobResponse), _ => Ok(GeneratedField::__SkipField__), } } @@ -43839,6 +44967,20 @@ impl<'de> serde::Deserialize<'de> for SignalResponse { return Err(serde::de::Error::duplicate_field("dataTrackSubscriberHandles")); } message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::DataTrackSubscriberHandles) +; + } + GeneratedField::StoreDataBlobResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("storeDataBlobResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::StoreDataBlobResponse) +; + } + GeneratedField::GetDataBlobResponse => { + if message__.is_some() { + return Err(serde::de::Error::duplicate_field("getDataBlobResponse")); + } + message__ = map_.next_value::<::std::option::Option<_>>()?.map(signal_response::Message::GetDataBlobResponse) ; } GeneratedField::__SkipField__ => { @@ -45403,6 +46545,236 @@ impl<'de> serde::Deserialize<'de> for StorageConfig { deserializer.deserialize_struct("livekit.StorageConfig", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for StoreDataBlobRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.request_id != 0 { + len += 1; + } + if self.blob.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.StoreDataBlobRequest", len)?; + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } + if let Some(v) = self.blob.as_ref() { + struct_ser.serialize_field("blob", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for StoreDataBlobRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "request_id", + "requestId", + "blob", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RequestId, + Blob, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "blob" => Ok(GeneratedField::Blob), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = StoreDataBlobRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.StoreDataBlobRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut request_id__ = None; + let mut blob__ = 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::Blob => { + if blob__.is_some() { + return Err(serde::de::Error::duplicate_field("blob")); + } + blob__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StoreDataBlobRequest { + request_id: request_id__.unwrap_or_default(), + blob: blob__, + }) + } + } + deserializer.deserialize_struct("livekit.StoreDataBlobRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for StoreDataBlobResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.request_id != 0 { + len += 1; + } + if self.key.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("livekit.StoreDataBlobResponse", len)?; + if self.request_id != 0 { + struct_ser.serialize_field("requestId", &self.request_id)?; + } + if let Some(v) = self.key.as_ref() { + struct_ser.serialize_field("key", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for StoreDataBlobResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "request_id", + "requestId", + "key", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + RequestId, + Key, + __SkipField__, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "requestId" | "request_id" => Ok(GeneratedField::RequestId), + "key" => Ok(GeneratedField::Key), + _ => Ok(GeneratedField::__SkipField__), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = StoreDataBlobResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct livekit.StoreDataBlobResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut request_id__ = None; + let mut key__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::RequestId => { + if request_id__.is_some() { + return Err(serde::de::Error::duplicate_field("requestId")); + } + request_id__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Key => { + if key__.is_some() { + return Err(serde::de::Error::duplicate_field("key")); + } + key__ = map_.next_value()?; + } + GeneratedField::__SkipField__ => { + let _ = map_.next_value::()?; + } + } + } + Ok(StoreDataBlobResponse { + request_id: request_id__.unwrap_or_default(), + key: key__, + }) + } + } + deserializer.deserialize_struct("livekit.StoreDataBlobResponse", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for StreamInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result From f9449ca2c169dfae7bd9052db4f612e78828a325 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:00:57 -0400 Subject: [PATCH 17/37] fix: downgrade protocol to before data track schema metadata I'll let jacob's in flight pull request push this all the way to the latest --- livekit-protocol/protocol | 2 +- livekit-protocol/src/livekit.rs | 187 +------- livekit-protocol/src/livekit.serde.rs | 620 -------------------------- 3 files changed, 4 insertions(+), 805 deletions(-) diff --git a/livekit-protocol/protocol b/livekit-protocol/protocol index 672b5c86a..39fc751df 160000 --- a/livekit-protocol/protocol +++ b/livekit-protocol/protocol @@ -1 +1 @@ -Subproject commit 672b5c86a96b47cb617cb0f3a9c06f73a4660c0a +Subproject commit 39fc751df610243c1bfdf85d3be6b3928ecef014 diff --git a/livekit-protocol/src/livekit.rs b/livekit-protocol/src/livekit.rs index ab6b74e52..ac2408d86 100644 --- a/livekit-protocol/src/livekit.rs +++ b/livekit-protocol/src/livekit.rs @@ -94,8 +94,6 @@ pub struct MetricsRecordingHeader { pub room_name: ::prost::alloc::string::String, #[prost(message, optional, tag="7")] pub room_start_time: ::core::option::Option<::pbjson_types::Timestamp>, - #[prost(string, tag="8")] - pub job_id: ::prost::alloc::string::String, } // // Protocol used to record metrics for a specific session. @@ -638,177 +636,6 @@ pub struct DataTrackInfo { /// Method used for end-to-end encryption (E2EE) on packet payloads. #[prost(enumeration="encryption::Type", tag="4")] pub encryption: i32, - /// Encoding for frame payloads on this track. If unspecified, the track is untyped. - #[prost(message, optional, tag="5")] - pub frame_encoding: ::core::option::Option, - /// ID of the schema used by frames on this track if the track is typed. - #[prost(message, optional, tag="6")] - pub schema: ::core::option::Option, -} -/// Encoding for frame payloads. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DataTrackFrameEncoding { - #[prost(oneof="data_track_frame_encoding::Value", tags="1, 2")] - pub value: ::core::option::Option, -} -/// Nested message and enum types in `DataTrackFrameEncoding`. -pub mod data_track_frame_encoding { - /// Well-known encoding for frame payloads. - /// - /// Mirrors the well-known message encodings from the MCAP spec: - /// - /// - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] - #[repr(i32)] - pub enum WellKnownFrameEncoding { - Unspecified = 0, - /// ROS 1: must be described by `ROS1_MSG` schema encoding. - Ros1 = 1, - /// CDR: must be described by `ROS2_MSG`, `ROS2_IDL`, or `OMG_IDL` schema encoding. - Cdr = 2, - /// Protocol Buffer: must be described by `PROTOBUF` schema encoding. - Protobuf = 3, - /// FlatBuffer: must be described by `FLATBUFFER` schema encoding. - Flatbuffer = 4, - /// CBOR: self-describing. - Cbor = 5, - /// MessagePack: self-describing. - Msgpack = 6, - /// JSON: self-describing or described by `JSON_SCHEMA` schema encoding. - Json = 7, - } - impl WellKnownFrameEncoding { - /// 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 { - WellKnownFrameEncoding::Unspecified => "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED", - WellKnownFrameEncoding::Ros1 => "WELL_KNOWN_FRAME_ENCODING_ROS1", - WellKnownFrameEncoding::Cdr => "WELL_KNOWN_FRAME_ENCODING_CDR", - WellKnownFrameEncoding::Protobuf => "WELL_KNOWN_FRAME_ENCODING_PROTOBUF", - WellKnownFrameEncoding::Flatbuffer => "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER", - WellKnownFrameEncoding::Cbor => "WELL_KNOWN_FRAME_ENCODING_CBOR", - WellKnownFrameEncoding::Msgpack => "WELL_KNOWN_FRAME_ENCODING_MSGPACK", - WellKnownFrameEncoding::Json => "WELL_KNOWN_FRAME_ENCODING_JSON", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED" => Some(Self::Unspecified), - "WELL_KNOWN_FRAME_ENCODING_ROS1" => Some(Self::Ros1), - "WELL_KNOWN_FRAME_ENCODING_CDR" => Some(Self::Cdr), - "WELL_KNOWN_FRAME_ENCODING_PROTOBUF" => Some(Self::Protobuf), - "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER" => Some(Self::Flatbuffer), - "WELL_KNOWN_FRAME_ENCODING_CBOR" => Some(Self::Cbor), - "WELL_KNOWN_FRAME_ENCODING_MSGPACK" => Some(Self::Msgpack), - "WELL_KNOWN_FRAME_ENCODING_JSON" => Some(Self::Json), - _ => None, - } - } - } - #[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Value { - #[prost(enumeration="WellKnownFrameEncoding", tag="1")] - WellKnown(i32), - /// Identifier of a custom encoding not covered by the well-known cases. - /// This must be non-empty and no longer than 32 characters. - #[prost(string, tag="2")] - Custom(::prost::alloc::string::String), - } -} -/// Encoding for schema definition. -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DataTrackSchemaEncoding { - #[prost(oneof="data_track_schema_encoding::Value", tags="1, 2")] - pub value: ::core::option::Option, -} -/// Nested message and enum types in `DataTrackSchemaEncoding`. -pub mod data_track_schema_encoding { - /// Well-known encoding for schema definition. - /// - /// Mirrors the well-known schema encodings from the MCAP spec: - /// - /// - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] - #[repr(i32)] - pub enum WellKnownSchemaEncoding { - Unspecified = 0, - /// Protocol Buffer IDL: describes `PROTOBUF` frame encoding. - Protobuf = 1, - /// FlatBuffer IDL: describes `FLATBUFFER` frame encoding. - Flatbuffer = 2, - /// ROS 1 Message: describes `ROS1` frame encoding. - Ros1Msg = 3, - /// ROS 2 Message: describes `CDR` frame encoding. - Ros2Msg = 4, - /// ROS 2 IDL: describes `CDR` frame encoding. - Ros2Idl = 5, - /// OMG IDL: describes `CDR` frame encoding. - OmgIdl = 6, - /// JSON Schema: describes `JSON` frame encoding. - JsonSchema = 7, - } - impl WellKnownSchemaEncoding { - /// 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 { - WellKnownSchemaEncoding::Unspecified => "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED", - WellKnownSchemaEncoding::Protobuf => "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF", - WellKnownSchemaEncoding::Flatbuffer => "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER", - WellKnownSchemaEncoding::Ros1Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG", - WellKnownSchemaEncoding::Ros2Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG", - WellKnownSchemaEncoding::Ros2Idl => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL", - WellKnownSchemaEncoding::OmgIdl => "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL", - WellKnownSchemaEncoding::JsonSchema => "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED" => Some(Self::Unspecified), - "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF" => Some(Self::Protobuf), - "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER" => Some(Self::Flatbuffer), - "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG" => Some(Self::Ros1Msg), - "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG" => Some(Self::Ros2Msg), - "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL" => Some(Self::Ros2Idl), - "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL" => Some(Self::OmgIdl), - "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA" => Some(Self::JsonSchema), - _ => None, - } - } - } - #[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Value { - #[prost(enumeration="WellKnownSchemaEncoding", tag="1")] - WellKnown(i32), - /// Identifier of a custom encoding not covered by the well-known cases. - /// This must be non-empty and no longer than 32 characters. - #[prost(string, tag="2")] - Custom(::prost::alloc::string::String), - } -} -/// Identifier for a data track schema. -/// -/// Schemas with the same name but different encodings are distinct. -/// -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DataTrackSchemaId { - /// This must be non-empty and no longer than 256 characters. - #[prost(string, tag="1")] - pub name: ::prost::alloc::string::String, - #[prost(message, optional, tag="2")] - pub encoding: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] @@ -830,7 +657,7 @@ pub struct DataTrackSubscriptionOptions { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct DataBlobKey { - #[prost(oneof="data_blob_key::Key", tags="1, 2")] + #[prost(oneof="data_blob_key::Key", tags="1")] pub key: ::core::option::Option, } /// Nested message and enum types in `DataBlobKey`. @@ -839,11 +666,10 @@ pub mod data_blob_key { #[derive(Clone, PartialEq, ::prost::Oneof)] pub enum Key { /// Generic string key, blob contains arbitrary data. + /// + /// Add additional key types here for storing specific types of blobs. #[prost(string, tag="1")] Generic(::prost::alloc::string::String), - /// Data track schema identifier, blob contains schema definition. - #[prost(message, tag="2")] - SchemaId(super::DataTrackSchemaId), } } /// A blob of data stored in a room identified by a unique key. @@ -4015,13 +3841,6 @@ pub struct PublishDataTrackRequest { /// Method used for end-to-end encryption (E2EE) on frame payloads. #[prost(enumeration="encryption::Type", tag="3")] pub encryption: i32, - /// Encoding for frame payloads on this track. If unspecified, the track is untyped. - #[prost(message, optional, tag="4")] - pub frame_encoding: ::core::option::Option, - /// ID of the schema used by frames on this track if the track is typed. - /// If set, the associated schema must be stored with `StoreDataBlobRequest`. - #[prost(message, optional, tag="5")] - pub schema: ::core::option::Option, } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/livekit-protocol/src/livekit.serde.rs b/livekit-protocol/src/livekit.serde.rs index 3ff3c3c0f..fa22db55a 100644 --- a/livekit-protocol/src/livekit.serde.rs +++ b/livekit-protocol/src/livekit.serde.rs @@ -10660,9 +10660,6 @@ impl serde::Serialize for DataBlobKey { data_blob_key::Key::Generic(v) => { struct_ser.serialize_field("generic", v)?; } - data_blob_key::Key::SchemaId(v) => { - struct_ser.serialize_field("schemaId", v)?; - } } } struct_ser.end() @@ -10676,14 +10673,11 @@ impl<'de> serde::Deserialize<'de> for DataBlobKey { { const FIELDS: &[&str] = &[ "generic", - "schema_id", - "schemaId", ]; #[allow(clippy::enum_variant_names)] enum GeneratedField { Generic, - SchemaId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -10707,7 +10701,6 @@ impl<'de> serde::Deserialize<'de> for DataBlobKey { { match value { "generic" => Ok(GeneratedField::Generic), - "schemaId" | "schema_id" => Ok(GeneratedField::SchemaId), _ => Ok(GeneratedField::__SkipField__), } } @@ -10736,13 +10729,6 @@ impl<'de> serde::Deserialize<'de> for DataBlobKey { } key__ = map_.next_value::<::std::option::Option<_>>()?.map(data_blob_key::Key::Generic); } - GeneratedField::SchemaId => { - if key__.is_some() { - return Err(serde::de::Error::duplicate_field("schemaId")); - } - key__ = map_.next_value::<::std::option::Option<_>>()?.map(data_blob_key::Key::SchemaId) -; - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -12932,209 +12918,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackExtensionParticipantSid { deserializer.deserialize_struct("livekit.DataTrackExtensionParticipantSid", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DataTrackFrameEncoding { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if self.value.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.DataTrackFrameEncoding", len)?; - if let Some(v) = self.value.as_ref() { - match v { - data_track_frame_encoding::Value::WellKnown(v) => { - let v = data_track_frame_encoding::WellKnownFrameEncoding::try_from(*v) - .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; - struct_ser.serialize_field("wellKnown", &v)?; - } - data_track_frame_encoding::Value::Custom(v) => { - struct_ser.serialize_field("custom", v)?; - } - } - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for DataTrackFrameEncoding { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "well_known", - "wellKnown", - "custom", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - WellKnown, - Custom, - __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 { - "wellKnown" | "well_known" => Ok(GeneratedField::WellKnown), - "custom" => Ok(GeneratedField::Custom), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DataTrackFrameEncoding; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DataTrackFrameEncoding") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut value__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::WellKnown => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("wellKnown")); - } - value__ = map_.next_value::<::std::option::Option>()?.map(|x| data_track_frame_encoding::Value::WellKnown(x as i32)); - } - GeneratedField::Custom => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("custom")); - } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(data_track_frame_encoding::Value::Custom); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(DataTrackFrameEncoding { - value: value__, - }) - } - } - deserializer.deserialize_struct("livekit.DataTrackFrameEncoding", FIELDS, GeneratedVisitor) - } -} -impl serde::Serialize for data_track_frame_encoding::WellKnownFrameEncoding { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - let variant = match self { - Self::Unspecified => "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED", - Self::Ros1 => "WELL_KNOWN_FRAME_ENCODING_ROS1", - Self::Cdr => "WELL_KNOWN_FRAME_ENCODING_CDR", - Self::Protobuf => "WELL_KNOWN_FRAME_ENCODING_PROTOBUF", - Self::Flatbuffer => "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER", - Self::Cbor => "WELL_KNOWN_FRAME_ENCODING_CBOR", - Self::Msgpack => "WELL_KNOWN_FRAME_ENCODING_MSGPACK", - Self::Json => "WELL_KNOWN_FRAME_ENCODING_JSON", - }; - serializer.serialize_str(variant) - } -} -impl<'de> serde::Deserialize<'de> for data_track_frame_encoding::WellKnownFrameEncoding { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED", - "WELL_KNOWN_FRAME_ENCODING_ROS1", - "WELL_KNOWN_FRAME_ENCODING_CDR", - "WELL_KNOWN_FRAME_ENCODING_PROTOBUF", - "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER", - "WELL_KNOWN_FRAME_ENCODING_CBOR", - "WELL_KNOWN_FRAME_ENCODING_MSGPACK", - "WELL_KNOWN_FRAME_ENCODING_JSON", - ]; - - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = data_track_frame_encoding::WellKnownFrameEncoding; - - 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 { - "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Unspecified), - "WELL_KNOWN_FRAME_ENCODING_ROS1" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Ros1), - "WELL_KNOWN_FRAME_ENCODING_CDR" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Cdr), - "WELL_KNOWN_FRAME_ENCODING_PROTOBUF" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Protobuf), - "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Flatbuffer), - "WELL_KNOWN_FRAME_ENCODING_CBOR" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Cbor), - "WELL_KNOWN_FRAME_ENCODING_MSGPACK" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Msgpack), - "WELL_KNOWN_FRAME_ENCODING_JSON" => Ok(data_track_frame_encoding::WellKnownFrameEncoding::Json), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), - } - } - } - deserializer.deserialize_any(GeneratedVisitor) - } -} impl serde::Serialize for DataTrackInfo { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -13155,12 +12938,6 @@ impl serde::Serialize for DataTrackInfo { if self.encryption != 0 { len += 1; } - if self.frame_encoding.is_some() { - len += 1; - } - if self.schema.is_some() { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.DataTrackInfo", len)?; if self.pub_handle != 0 { struct_ser.serialize_field("pubHandle", &self.pub_handle)?; @@ -13176,12 +12953,6 @@ impl serde::Serialize for DataTrackInfo { .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; struct_ser.serialize_field("encryption", &v)?; } - if let Some(v) = self.frame_encoding.as_ref() { - struct_ser.serialize_field("frameEncoding", v)?; - } - if let Some(v) = self.schema.as_ref() { - struct_ser.serialize_field("schema", v)?; - } struct_ser.end() } } @@ -13197,9 +12968,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { "sid", "name", "encryption", - "frame_encoding", - "frameEncoding", - "schema", ]; #[allow(clippy::enum_variant_names)] @@ -13208,8 +12976,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { Sid, Name, Encryption, - FrameEncoding, - Schema, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -13236,8 +13002,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { "sid" => Ok(GeneratedField::Sid), "name" => Ok(GeneratedField::Name), "encryption" => Ok(GeneratedField::Encryption), - "frameEncoding" | "frame_encoding" => Ok(GeneratedField::FrameEncoding), - "schema" => Ok(GeneratedField::Schema), _ => Ok(GeneratedField::__SkipField__), } } @@ -13261,8 +13025,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { let mut sid__ = None; let mut name__ = None; let mut encryption__ = None; - let mut frame_encoding__ = None; - let mut schema__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PubHandle => { @@ -13291,18 +13053,6 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { } encryption__ = Some(map_.next_value::()? as i32); } - GeneratedField::FrameEncoding => { - if frame_encoding__.is_some() { - return Err(serde::de::Error::duplicate_field("frameEncoding")); - } - frame_encoding__ = map_.next_value()?; - } - GeneratedField::Schema => { - if schema__.is_some() { - return Err(serde::de::Error::duplicate_field("schema")); - } - schema__ = map_.next_value()?; - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -13313,329 +13063,12 @@ impl<'de> serde::Deserialize<'de> for DataTrackInfo { sid: sid__.unwrap_or_default(), name: name__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), - frame_encoding: frame_encoding__, - schema: schema__, }) } } deserializer.deserialize_struct("livekit.DataTrackInfo", FIELDS, GeneratedVisitor) } } -impl serde::Serialize for DataTrackSchemaEncoding { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if self.value.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSchemaEncoding", len)?; - if let Some(v) = self.value.as_ref() { - match v { - data_track_schema_encoding::Value::WellKnown(v) => { - let v = data_track_schema_encoding::WellKnownSchemaEncoding::try_from(*v) - .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", *v)))?; - struct_ser.serialize_field("wellKnown", &v)?; - } - data_track_schema_encoding::Value::Custom(v) => { - struct_ser.serialize_field("custom", v)?; - } - } - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for DataTrackSchemaEncoding { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "well_known", - "wellKnown", - "custom", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - WellKnown, - Custom, - __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 { - "wellKnown" | "well_known" => Ok(GeneratedField::WellKnown), - "custom" => Ok(GeneratedField::Custom), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DataTrackSchemaEncoding; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DataTrackSchemaEncoding") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut value__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::WellKnown => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("wellKnown")); - } - value__ = map_.next_value::<::std::option::Option>()?.map(|x| data_track_schema_encoding::Value::WellKnown(x as i32)); - } - GeneratedField::Custom => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("custom")); - } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(data_track_schema_encoding::Value::Custom); - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(DataTrackSchemaEncoding { - value: value__, - }) - } - } - deserializer.deserialize_struct("livekit.DataTrackSchemaEncoding", FIELDS, GeneratedVisitor) - } -} -impl serde::Serialize for data_track_schema_encoding::WellKnownSchemaEncoding { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - let variant = match self { - Self::Unspecified => "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED", - Self::Protobuf => "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF", - Self::Flatbuffer => "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER", - Self::Ros1Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG", - Self::Ros2Msg => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG", - Self::Ros2Idl => "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL", - Self::OmgIdl => "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL", - Self::JsonSchema => "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA", - }; - serializer.serialize_str(variant) - } -} -impl<'de> serde::Deserialize<'de> for data_track_schema_encoding::WellKnownSchemaEncoding { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED", - "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF", - "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER", - "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG", - "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG", - "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL", - "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL", - "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA", - ]; - - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = data_track_schema_encoding::WellKnownSchemaEncoding; - - 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 { - "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Unspecified), - "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Protobuf), - "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Flatbuffer), - "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Ros1Msg), - "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Ros2Msg), - "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::Ros2Idl), - "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::OmgIdl), - "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA" => Ok(data_track_schema_encoding::WellKnownSchemaEncoding::JsonSchema), - _ => Err(serde::de::Error::unknown_variant(value, FIELDS)), - } - } - } - deserializer.deserialize_any(GeneratedVisitor) - } -} -impl serde::Serialize for DataTrackSchemaId { - #[allow(deprecated)] - fn serialize(&self, serializer: S) -> std::result::Result - where - S: serde::Serializer, - { - use serde::ser::SerializeStruct; - let mut len = 0; - if !self.name.is_empty() { - len += 1; - } - if self.encoding.is_some() { - len += 1; - } - let mut struct_ser = serializer.serialize_struct("livekit.DataTrackSchemaId", len)?; - if !self.name.is_empty() { - struct_ser.serialize_field("name", &self.name)?; - } - if let Some(v) = self.encoding.as_ref() { - struct_ser.serialize_field("encoding", v)?; - } - struct_ser.end() - } -} -impl<'de> serde::Deserialize<'de> for DataTrackSchemaId { - #[allow(deprecated)] - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - const FIELDS: &[&str] = &[ - "name", - "encoding", - ]; - - #[allow(clippy::enum_variant_names)] - enum GeneratedField { - Name, - Encoding, - __SkipField__, - } - impl<'de> serde::Deserialize<'de> for GeneratedField { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - struct GeneratedVisitor; - - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = GeneratedField; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(formatter, "expected one of: {:?}", &FIELDS) - } - - #[allow(unused_variables)] - fn visit_str(self, value: &str) -> std::result::Result - where - E: serde::de::Error, - { - match value { - "name" => Ok(GeneratedField::Name), - "encoding" => Ok(GeneratedField::Encoding), - _ => Ok(GeneratedField::__SkipField__), - } - } - } - deserializer.deserialize_identifier(GeneratedVisitor) - } - } - struct GeneratedVisitor; - impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { - type Value = DataTrackSchemaId; - - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str("struct livekit.DataTrackSchemaId") - } - - fn visit_map(self, mut map_: V) -> std::result::Result - where - V: serde::de::MapAccess<'de>, - { - let mut name__ = None; - let mut encoding__ = None; - while let Some(k) = map_.next_key()? { - match k { - GeneratedField::Name => { - if name__.is_some() { - return Err(serde::de::Error::duplicate_field("name")); - } - name__ = Some(map_.next_value()?); - } - GeneratedField::Encoding => { - if encoding__.is_some() { - return Err(serde::de::Error::duplicate_field("encoding")); - } - encoding__ = map_.next_value()?; - } - GeneratedField::__SkipField__ => { - let _ = map_.next_value::()?; - } - } - } - Ok(DataTrackSchemaId { - name: name__.unwrap_or_default(), - encoding: encoding__, - }) - } - } - deserializer.deserialize_struct("livekit.DataTrackSchemaId", FIELDS, GeneratedVisitor) - } -} impl serde::Serialize for DataTrackSubscriberHandles { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result @@ -27129,9 +26562,6 @@ impl serde::Serialize for MetricsRecordingHeader { if self.room_start_time.is_some() { len += 1; } - if !self.job_id.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)?; @@ -27153,9 +26583,6 @@ impl serde::Serialize for MetricsRecordingHeader { if let Some(v) = self.room_start_time.as_ref() { struct_ser.serialize_field("roomStartTime", v)?; } - if !self.job_id.is_empty() { - struct_ser.serialize_field("jobId", &self.job_id)?; - } struct_ser.end() } } @@ -27177,8 +26604,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "roomName", "room_start_time", "roomStartTime", - "job_id", - "jobId", ]; #[allow(clippy::enum_variant_names)] @@ -27189,7 +26614,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { RoomTags, RoomName, RoomStartTime, - JobId, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -27218,7 +26642,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { "roomTags" | "room_tags" => Ok(GeneratedField::RoomTags), "roomName" | "room_name" => Ok(GeneratedField::RoomName), "roomStartTime" | "room_start_time" => Ok(GeneratedField::RoomStartTime), - "jobId" | "job_id" => Ok(GeneratedField::JobId), _ => Ok(GeneratedField::__SkipField__), } } @@ -27244,7 +26667,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { let mut room_tags__ = None; let mut room_name__ = None; let mut room_start_time__ = None; - let mut job_id__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::RoomId => { @@ -27287,12 +26709,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { } room_start_time__ = map_.next_value()?; } - GeneratedField::JobId => { - if job_id__.is_some() { - return Err(serde::de::Error::duplicate_field("jobId")); - } - job_id__ = Some(map_.next_value()?); - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -27305,7 +26721,6 @@ impl<'de> serde::Deserialize<'de> for MetricsRecordingHeader { room_tags: room_tags__.unwrap_or_default(), room_name: room_name__.unwrap_or_default(), room_start_time: room_start_time__, - job_id: job_id__.unwrap_or_default(), }) } } @@ -30830,12 +30245,6 @@ impl serde::Serialize for PublishDataTrackRequest { if self.encryption != 0 { len += 1; } - if self.frame_encoding.is_some() { - len += 1; - } - if self.schema.is_some() { - len += 1; - } let mut struct_ser = serializer.serialize_struct("livekit.PublishDataTrackRequest", len)?; if self.pub_handle != 0 { struct_ser.serialize_field("pubHandle", &self.pub_handle)?; @@ -30848,12 +30257,6 @@ impl serde::Serialize for PublishDataTrackRequest { .map_err(|_| serde::ser::Error::custom(format!("Invalid variant {}", self.encryption)))?; struct_ser.serialize_field("encryption", &v)?; } - if let Some(v) = self.frame_encoding.as_ref() { - struct_ser.serialize_field("frameEncoding", v)?; - } - if let Some(v) = self.schema.as_ref() { - struct_ser.serialize_field("schema", v)?; - } struct_ser.end() } } @@ -30868,9 +30271,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { "pubHandle", "name", "encryption", - "frame_encoding", - "frameEncoding", - "schema", ]; #[allow(clippy::enum_variant_names)] @@ -30878,8 +30278,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { PubHandle, Name, Encryption, - FrameEncoding, - Schema, __SkipField__, } impl<'de> serde::Deserialize<'de> for GeneratedField { @@ -30905,8 +30303,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { "pubHandle" | "pub_handle" => Ok(GeneratedField::PubHandle), "name" => Ok(GeneratedField::Name), "encryption" => Ok(GeneratedField::Encryption), - "frameEncoding" | "frame_encoding" => Ok(GeneratedField::FrameEncoding), - "schema" => Ok(GeneratedField::Schema), _ => Ok(GeneratedField::__SkipField__), } } @@ -30929,8 +30325,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { let mut pub_handle__ = None; let mut name__ = None; let mut encryption__ = None; - let mut frame_encoding__ = None; - let mut schema__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::PubHandle => { @@ -30953,18 +30347,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { } encryption__ = Some(map_.next_value::()? as i32); } - GeneratedField::FrameEncoding => { - if frame_encoding__.is_some() { - return Err(serde::de::Error::duplicate_field("frameEncoding")); - } - frame_encoding__ = map_.next_value()?; - } - GeneratedField::Schema => { - if schema__.is_some() { - return Err(serde::de::Error::duplicate_field("schema")); - } - schema__ = map_.next_value()?; - } GeneratedField::__SkipField__ => { let _ = map_.next_value::()?; } @@ -30974,8 +30356,6 @@ impl<'de> serde::Deserialize<'de> for PublishDataTrackRequest { pub_handle: pub_handle__.unwrap_or_default(), name: name__.unwrap_or_default(), encryption: encryption__.unwrap_or_default(), - frame_encoding: frame_encoding__, - schema: schema__, }) } } From 37687234d3fa7415752aeac23b771a15c59ed1f6 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:04:47 -0400 Subject: [PATCH 18/37] refactor: convert data stream open tuple to struct and add is_compressed / is_inline --- livekit/src/room/data_stream/incoming.rs | 57 ++++++++++++++++-------- livekit/src/room/mod.rs | 22 ++++++--- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index b1fc5f525..5f590ed69 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -308,10 +308,25 @@ fn inflate_raw(data: &[u8]) -> StreamResult> { Ok(out) } +/// Metadata about a data stream which has just been opened +pub(crate) struct DataStreamOpenInfo { + pub(crate) reader: AnyStreamReader, + pub(crate) identity: String, + + /// Whether the payload was deflate-raw compressed (data streams v2). + #[allow(unused)] + pub(crate) is_compressed: bool, + + /// Whether the whole payload was sent inline in the header as a single packet + /// (data streams v2), rather than as separate chunk packets. + #[allow(unused)] + pub(crate) is_inline: bool, +} + #[derive(Clone)] pub(crate) struct IncomingStreamManager { inner: Arc>, - open_tx: UnboundedSender<(AnyStreamReader, String)>, + open_tx: UnboundedSender, } #[derive(Default)] @@ -320,7 +335,7 @@ struct ManagerInner { } impl IncomingStreamManager { - pub fn new() -> (Self, UnboundedReceiver<(AnyStreamReader, String)>) { + pub fn new() -> (Self, UnboundedReceiver) { let (open_tx, open_rx) = mpsc::unbounded_channel(); (Self { inner: Arc::new(Mutex::new(Default::default())), open_tx }, open_rx) } @@ -355,7 +370,12 @@ impl IncomingStreamManager { } let (reader, chunk_tx) = AnyStreamReader::from(info); - let _ = self.open_tx.send((reader, identity)); + let _ = self.open_tx.send(DataStreamOpenInfo { + reader, + identity, + is_compressed, + is_inline: inline_content.is_some(), + }); // Inline single-packet stream: synthesize the complete content now; no chunk/trailer // packets will follow, so we never register an open descriptor. @@ -635,9 +655,7 @@ mod tests { proto::Trailer { stream_id: id.to_string(), reason: String::new(), attributes } } - async fn recv_reader( - rx: &mut UnboundedReceiver<(AnyStreamReader, String)>, - ) -> (AnyStreamReader, String) { + async fn recv_reader(rx: &mut UnboundedReceiver) -> DataStreamOpenInfo { rx.recv().await.expect("a reader should be dispatched") } @@ -679,8 +697,11 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, identity) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, identity, is_compressed, is_inline } = + recv_reader(&mut rx).await; assert_eq!(identity, SENDER); + assert_eq!(compressed, false); + assert_eq!(inline, false); assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None); mgr.handle_trailer(trailer("s1")); @@ -695,7 +716,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4]), EncType::None); mgr.handle_trailer(trailer("s1")); assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3, 4])); @@ -716,7 +737,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None); mgr.handle_trailer(trailer_with_attrs( "s1", @@ -737,7 +758,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![b'x']), EncType::None); mgr.handle_trailer(trailer("s1")); assert!(matches!(read_text(reader).await, Err(StreamError::Incomplete))); @@ -751,7 +772,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4, 5]), EncType::None); mgr.handle_trailer(trailer("s1")); assert!(matches!(read_bytes(reader).await, Err(StreamError::LengthExceeded))); @@ -765,7 +786,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![b'h', b'i']), EncType::Gcm); assert!(matches!(read_text(reader).await, Err(StreamError::EncryptionTypeMismatch))); } @@ -787,7 +808,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); // No chunk/trailer packets are fed. assert_eq!(read_text(reader).await.unwrap(), text); @@ -801,7 +822,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3])); } @@ -821,7 +842,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); assert_eq!(read_text(reader).await.unwrap(), text); } @@ -841,7 +862,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(payload)); } @@ -867,7 +888,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; for (i, piece) in chunk_pieces.iter().enumerate() { mgr.handle_chunk(chunk("s1", i as u64, piece.to_vec()), EncType::None); } @@ -893,7 +914,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let (reader, _) = recv_reader(&mut rx).await; + let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, pieces[0].to_vec()), EncType::None); // Skip index 1 -> feed index 2: a gap is a hard error. mgr.handle_chunk(chunk("s1", 2, pieces[1].to_vec()), EncType::None); diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index fdabad527..4bd4cb740 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -219,6 +219,12 @@ pub enum RoomEvent { reader: TakeCell, topic: String, participant_identity: ParticipantIdentity, + /// Test-only: expose whether the byte stream is being compressed or not. + #[cfg(feature = "__lk-e2e-test")] + is_compressed: bool, + /// Test-only: expose whether the byte stream has been sent inline on the header packet + #[cfg(feature = "__lk-e2e-test")] + is_inline: bool, }, TextStreamOpened { reader: TakeCell, @@ -2263,22 +2269,26 @@ impl rpc::RemoteParticipantRegistry for RoomSession { /// Intercepts text streams on RPC topics (`lk.rpc_request`, `lk.rpc_response`) /// and routes them to the RPC managers instead of emitting them as room events. async fn incoming_data_stream_task( - mut open_rx: UnboundedReceiver<(AnyStreamReader, String)>, + mut open_rx: UnboundedReceiver, dispatcher: Dispatcher, mut close_rx: broadcast::Receiver<()>, session: Arc, ) { loop { tokio::select! { - Some((reader, identity)) = open_rx.recv() => { - match reader { + Some(open_info) = open_rx.recv() => { + match open_info.reader { AnyStreamReader::Byte(reader) => { let topic = reader.info().topic.clone(); if !data_stream::is_internal_topic(&topic) { dispatcher.dispatch(&RoomEvent::ByteStreamOpened { topic, reader: TakeCell::new(reader), - participant_identity: ParticipantIdentity(identity) + participant_identity: ParticipantIdentity(open_info.identity), + #[cfg(feature = "__lk-e2e-test")] + is_compressed: open_info.is_compressed, + #[cfg(feature = "__lk-e2e-test")] + is_inline: open_info.is_inline, }); } } @@ -2286,7 +2296,7 @@ async fn incoming_data_stream_task( let topic = reader.info().topic.clone(); match topic.as_str() { rpc::RPC_REQUEST_TOPIC => { - let caller_identity = ParticipantIdentity(identity); + let caller_identity = ParticipantIdentity(open_info.identity); let session = session.clone(); livekit_runtime::spawn(async move { let transport = rpc::SessionTransport(session.clone()); @@ -2308,7 +2318,7 @@ async fn incoming_data_stream_task( dispatcher.dispatch(&RoomEvent::TextStreamOpened { topic, reader: TakeCell::new(reader), - participant_identity: ParticipantIdentity(identity) + participant_identity: ParticipantIdentity(open_info.identity) }); } } From db5c6aa0d8485a345fcf84a1b836ed94ad4ab811 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:15:14 -0400 Subject: [PATCH 19/37] feat: add is_compressed / is_inline to ByteStreamInfo / TextStreamInfo --- livekit/src/room/data_stream/mod.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/livekit/src/room/data_stream/mod.rs b/livekit/src/room/data_stream/mod.rs index 43bac0a3c..2c589faad 100644 --- a/livekit/src/room/data_stream/mod.rs +++ b/livekit/src/room/data_stream/mod.rs @@ -120,6 +120,12 @@ pub struct ByteStreamInfo { pub name: String, /// The encryption used pub encryption_type: EncryptionType, + /// Test-only: expose whether the byte stream was compressed or not. + // #[cfg(feature = "__lk-e2e-test")] + is_compressed: bool, + /// Test-only: expose whether the byte stream was sent inline on the header packet + // #[cfg(feature = "__lk-e2e-test")] + is_inline: bool, } /// Information about a text data stream. @@ -144,6 +150,12 @@ pub struct TextStreamInfo { pub generated: bool, /// The encryption used pub encryption_type: EncryptionType, + /// Test-only: expose whether the byte stream was compressed or not. + // #[cfg(feature = "__lk-e2e-test")] + is_compressed: bool, + /// Test-only: expose whether the byte stream was sent inline on the header packet + // #[cfg(feature = "__lk-e2e-test")] + is_inline: bool, } /// Operation type for text streams. @@ -196,6 +208,8 @@ impl ByteStreamInfo { byte_header: proto::ByteHeader, encryption_type: EncryptionType, ) -> Self { + let is_compressed = header.compression() != proto::CompressionType::None; + let is_inline = !header.inline_content().is_empty(); Self { id: header.stream_id, topic: header.topic, @@ -206,6 +220,8 @@ impl ByteStreamInfo { mime_type: header.mime_type, name: byte_header.name, encryption_type, + is_compressed, + is_inline, } } } @@ -220,6 +236,8 @@ impl TextStreamInfo { text_header: proto::TextHeader, encryption_type: EncryptionType, ) -> Self { + let was_compressed = header.compression() != proto::CompressionType::None; + let was_inline = !header.inline_content().is_empty(); Self { id: header.stream_id, topic: header.topic, @@ -235,6 +253,8 @@ impl TextStreamInfo { attached_stream_ids: text_header.attached_stream_ids, generated: text_header.generated, encryption_type, + is_compressed: was_compressed, + is_inline: was_inline, } } } From e8d64d27b02f124b91ea30204916a972e8b35dc9 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:16:16 -0400 Subject: [PATCH 20/37] Revert "refactor: convert data stream open tuple to struct and add is_compressed / is_inline" This reverts commit 37687234d3fa7415752aeac23b771a15c59ed1f6. --- livekit/src/room/data_stream/incoming.rs | 57 ++++++++---------------- livekit/src/room/mod.rs | 22 +++------ 2 files changed, 24 insertions(+), 55 deletions(-) diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index 5f590ed69..b1fc5f525 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -308,25 +308,10 @@ fn inflate_raw(data: &[u8]) -> StreamResult> { Ok(out) } -/// Metadata about a data stream which has just been opened -pub(crate) struct DataStreamOpenInfo { - pub(crate) reader: AnyStreamReader, - pub(crate) identity: String, - - /// Whether the payload was deflate-raw compressed (data streams v2). - #[allow(unused)] - pub(crate) is_compressed: bool, - - /// Whether the whole payload was sent inline in the header as a single packet - /// (data streams v2), rather than as separate chunk packets. - #[allow(unused)] - pub(crate) is_inline: bool, -} - #[derive(Clone)] pub(crate) struct IncomingStreamManager { inner: Arc>, - open_tx: UnboundedSender, + open_tx: UnboundedSender<(AnyStreamReader, String)>, } #[derive(Default)] @@ -335,7 +320,7 @@ struct ManagerInner { } impl IncomingStreamManager { - pub fn new() -> (Self, UnboundedReceiver) { + pub fn new() -> (Self, UnboundedReceiver<(AnyStreamReader, String)>) { let (open_tx, open_rx) = mpsc::unbounded_channel(); (Self { inner: Arc::new(Mutex::new(Default::default())), open_tx }, open_rx) } @@ -370,12 +355,7 @@ impl IncomingStreamManager { } let (reader, chunk_tx) = AnyStreamReader::from(info); - let _ = self.open_tx.send(DataStreamOpenInfo { - reader, - identity, - is_compressed, - is_inline: inline_content.is_some(), - }); + let _ = self.open_tx.send((reader, identity)); // Inline single-packet stream: synthesize the complete content now; no chunk/trailer // packets will follow, so we never register an open descriptor. @@ -655,7 +635,9 @@ mod tests { proto::Trailer { stream_id: id.to_string(), reason: String::new(), attributes } } - async fn recv_reader(rx: &mut UnboundedReceiver) -> DataStreamOpenInfo { + async fn recv_reader( + rx: &mut UnboundedReceiver<(AnyStreamReader, String)>, + ) -> (AnyStreamReader, String) { rx.recv().await.expect("a reader should be dispatched") } @@ -697,11 +679,8 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, identity, is_compressed, is_inline } = - recv_reader(&mut rx).await; + let (reader, identity) = recv_reader(&mut rx).await; assert_eq!(identity, SENDER); - assert_eq!(compressed, false); - assert_eq!(inline, false); assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None); mgr.handle_trailer(trailer("s1")); @@ -716,7 +695,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4]), EncType::None); mgr.handle_trailer(trailer("s1")); assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3, 4])); @@ -737,7 +716,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None); mgr.handle_trailer(trailer_with_attrs( "s1", @@ -758,7 +737,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![b'x']), EncType::None); mgr.handle_trailer(trailer("s1")); assert!(matches!(read_text(reader).await, Err(StreamError::Incomplete))); @@ -772,7 +751,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4, 5]), EncType::None); mgr.handle_trailer(trailer("s1")); assert!(matches!(read_bytes(reader).await, Err(StreamError::LengthExceeded))); @@ -786,7 +765,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, vec![b'h', b'i']), EncType::Gcm); assert!(matches!(read_text(reader).await, Err(StreamError::EncryptionTypeMismatch))); } @@ -808,7 +787,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); // No chunk/trailer packets are fed. assert_eq!(read_text(reader).await.unwrap(), text); @@ -822,7 +801,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3])); } @@ -842,7 +821,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string())); assert_eq!(read_text(reader).await.unwrap(), text); } @@ -862,7 +841,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(payload)); } @@ -888,7 +867,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; for (i, piece) in chunk_pieces.iter().enumerate() { mgr.handle_chunk(chunk("s1", i as u64, piece.to_vec()), EncType::None); } @@ -914,7 +893,7 @@ mod tests { SENDER.to_string(), EncType::None, ); - let DataStreamOpenInfo { reader, .. } = recv_reader(&mut rx).await; + let (reader, _) = recv_reader(&mut rx).await; mgr.handle_chunk(chunk("s1", 0, pieces[0].to_vec()), EncType::None); // Skip index 1 -> feed index 2: a gap is a hard error. mgr.handle_chunk(chunk("s1", 2, pieces[1].to_vec()), EncType::None); diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 4bd4cb740..fdabad527 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -219,12 +219,6 @@ pub enum RoomEvent { reader: TakeCell, topic: String, participant_identity: ParticipantIdentity, - /// Test-only: expose whether the byte stream is being compressed or not. - #[cfg(feature = "__lk-e2e-test")] - is_compressed: bool, - /// Test-only: expose whether the byte stream has been sent inline on the header packet - #[cfg(feature = "__lk-e2e-test")] - is_inline: bool, }, TextStreamOpened { reader: TakeCell, @@ -2269,26 +2263,22 @@ impl rpc::RemoteParticipantRegistry for RoomSession { /// Intercepts text streams on RPC topics (`lk.rpc_request`, `lk.rpc_response`) /// and routes them to the RPC managers instead of emitting them as room events. async fn incoming_data_stream_task( - mut open_rx: UnboundedReceiver, + mut open_rx: UnboundedReceiver<(AnyStreamReader, String)>, dispatcher: Dispatcher, mut close_rx: broadcast::Receiver<()>, session: Arc, ) { loop { tokio::select! { - Some(open_info) = open_rx.recv() => { - match open_info.reader { + Some((reader, identity)) = open_rx.recv() => { + match reader { AnyStreamReader::Byte(reader) => { let topic = reader.info().topic.clone(); if !data_stream::is_internal_topic(&topic) { dispatcher.dispatch(&RoomEvent::ByteStreamOpened { topic, reader: TakeCell::new(reader), - participant_identity: ParticipantIdentity(open_info.identity), - #[cfg(feature = "__lk-e2e-test")] - is_compressed: open_info.is_compressed, - #[cfg(feature = "__lk-e2e-test")] - is_inline: open_info.is_inline, + participant_identity: ParticipantIdentity(identity) }); } } @@ -2296,7 +2286,7 @@ async fn incoming_data_stream_task( let topic = reader.info().topic.clone(); match topic.as_str() { rpc::RPC_REQUEST_TOPIC => { - let caller_identity = ParticipantIdentity(open_info.identity); + let caller_identity = ParticipantIdentity(identity); let session = session.clone(); livekit_runtime::spawn(async move { let transport = rpc::SessionTransport(session.clone()); @@ -2318,7 +2308,7 @@ async fn incoming_data_stream_task( dispatcher.dispatch(&RoomEvent::TextStreamOpened { topic, reader: TakeCell::new(reader), - participant_identity: ParticipantIdentity(open_info.identity) + participant_identity: ParticipantIdentity(identity) }); } } From c70a15e0252f2abf9ae65425b23116661fb925d7 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:27:40 -0400 Subject: [PATCH 21/37] feat: explicitly check is_compressed / is_inline in e2e tests --- livekit/src/room/data_stream/mod.rs | 1 + livekit/tests/data_stream_test.rs | 46 +++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/livekit/src/room/data_stream/mod.rs b/livekit/src/room/data_stream/mod.rs index 2c589faad..1d488bf54 100644 --- a/livekit/src/room/data_stream/mod.rs +++ b/livekit/src/room/data_stream/mod.rs @@ -14,6 +14,7 @@ use chrono::{DateTime, Utc}; use libwebrtc::enum_dispatch; +use libwebrtc::native::create_random_uuid; use livekit_protocol::data_stream as proto; use std::collections::HashMap; use thiserror::Error; diff --git a/livekit/tests/data_stream_test.rs b/livekit/tests/data_stream_test.rs index 240a142e8..ce19a0b75 100644 --- a/livekit/tests/data_stream_test.rs +++ b/livekit/tests/data_stream_test.rs @@ -17,6 +17,7 @@ use { crate::common::test_rooms, anyhow::{anyhow, Ok, Result}, chrono::{TimeDelta, Utc}, + libwebrtc::native::create_random_uuid, livekit::{RoomEvent, StreamByteOptions, StreamReader, StreamTextOptions}, std::time::Duration, tokio::{time::timeout, try_join}, @@ -46,6 +47,8 @@ async fn test_send_bytes() -> Result<()> { assert!(stream_info.total_length.is_some()); assert_eq!(stream_info.mime_type, "application/octet-stream"); assert_eq!(stream_info.topic, "some-topic"); + assert_eq!(stream_info.is_compressed, false); + assert_eq!(stream_info.is_inline, true); Ok(()) }; @@ -92,7 +95,9 @@ async fn test_send_large_compressible_text() -> Result<()> { let send = async move { let options = StreamTextOptions { topic: "some-topic".into(), ..Default::default() }; - sending_room.local_participant().send_text(&text, options).await?; + let stream_info = sending_room.local_participant().send_text(&text, options).await?; + assert_eq!(stream_info.is_compressed, true); + assert_eq!(stream_info.is_inline, false); Ok(()) }; let receive = async move { @@ -112,6 +117,41 @@ async fn test_send_large_compressible_text() -> Result<()> { Ok(()) } +/// End-to-end round-trip of a large in-memory byte payload, validating the v2 byte-stream path. +#[cfg(feature = "__lk-e2e-test")] +#[tokio::test] +async fn test_send_large_incompressible_random_bytes() -> Result<()> { + let mut rooms = test_rooms(2).await?; + let (sending_room, _) = rooms.pop().unwrap(); + let (_, mut receiving_event_rx) = rooms.pop().unwrap(); + + let payload: Vec = (0..50_000).flat_map(|_| create_random_uuid().into_bytes()).collect(); + let expected = payload.clone(); + + let send = async move { + let options = StreamByteOptions { topic: "some-topic".into(), ..Default::default() }; + let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; + assert_eq!(stream_info.is_compressed, false); + assert_eq!(stream_info.is_inline, false); + Ok(()) + }; + let receive = async move { + while let Some(event) = receiving_event_rx.recv().await { + let RoomEvent::ByteStreamOpened { reader, topic, .. } = event else { + continue; + }; + assert_eq!(topic, "some-topic"); + let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?; + assert_eq!(reader.read_all().await?, expected); + break; + } + Ok(()) + }; + + timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??; + Ok(()) +} + /// End-to-end round-trip of a large in-memory byte payload, validating the v2 byte-stream path. #[cfg(feature = "__lk-e2e-test")] #[tokio::test] @@ -125,7 +165,9 @@ async fn test_send_large_bytes() -> Result<()> { let send = async move { let options = StreamByteOptions { topic: "some-topic".into(), ..Default::default() }; - sending_room.local_participant().send_bytes(&payload, options).await?; + let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; + assert_eq!(stream_info.is_compressed, true); + assert_eq!(stream_info.is_inline, false); Ok(()) }; let receive = async move { From c8d4a44df2424f3e7609924577f1dd82ac0c66fb Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:35:27 -0400 Subject: [PATCH 22/37] fix: add e2e test that tests compress=false path --- livekit/tests/data_stream_test.rs | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/livekit/tests/data_stream_test.rs b/livekit/tests/data_stream_test.rs index ce19a0b75..12b1daf75 100644 --- a/livekit/tests/data_stream_test.rs +++ b/livekit/tests/data_stream_test.rs @@ -187,6 +187,46 @@ async fn test_send_large_bytes() -> Result<()> { Ok(()) } +/// End-to-end round-trip of a large in-memory byte payload set with compress=false doesn't +/// compress the payload +#[cfg(feature = "__lk-e2e-test")] +#[tokio::test] +async fn test_data_stream_compress_false() -> Result<()> { + let mut rooms = test_rooms(2).await?; + let (sending_room, _) = rooms.pop().unwrap(); + let (_, mut receiving_event_rx) = rooms.pop().unwrap(); + + let payload = vec![0xFFu8; 50_000]; + let expected = payload.clone(); + + let send = async move { + let options = StreamByteOptions { + topic: "some-topic".into(), + compress: Some(false), // <= Explictly disable compression + ..Default::default() + }; + let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; + assert_eq!(stream_info.is_compressed, false); + assert_eq!(stream_info.is_inline, false); + Ok(()) + }; + let receive = async move { + while let Some(event) = receiving_event_rx.recv().await { + let RoomEvent::ByteStreamOpened { reader, topic, .. } = event else { + continue; + }; + assert_eq!(topic, "some-topic"); + let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?; + assert_eq!(reader.read_all().await?, expected); + break; + } + Ok(()) + }; + + timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??; + Ok(()) +} + #[cfg(feature = "__lk-e2e-test")] #[tokio::test] async fn test_send_text() -> Result<()> { From 03c81b3ae73d1d55f460ecf30c9d84d1267f2cab Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 15:49:30 -0400 Subject: [PATCH 23/37] feat: add e2e feature on compressed / inline fields --- livekit/src/room/data_stream/mod.rs | 34 +++++++++++++++-------------- livekit/src/room/rpc/tests.rs | 8 +++++++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/livekit/src/room/data_stream/mod.rs b/livekit/src/room/data_stream/mod.rs index 1d488bf54..a3d9c5182 100644 --- a/livekit/src/room/data_stream/mod.rs +++ b/livekit/src/room/data_stream/mod.rs @@ -122,11 +122,11 @@ pub struct ByteStreamInfo { /// The encryption used pub encryption_type: EncryptionType, /// Test-only: expose whether the byte stream was compressed or not. - // #[cfg(feature = "__lk-e2e-test")] - is_compressed: bool, + #[cfg(feature = "__lk-e2e-test")] + pub is_compressed: bool, /// Test-only: expose whether the byte stream was sent inline on the header packet - // #[cfg(feature = "__lk-e2e-test")] - is_inline: bool, + #[cfg(feature = "__lk-e2e-test")] + pub is_inline: bool, } /// Information about a text data stream. @@ -152,11 +152,11 @@ pub struct TextStreamInfo { /// The encryption used pub encryption_type: EncryptionType, /// Test-only: expose whether the byte stream was compressed or not. - // #[cfg(feature = "__lk-e2e-test")] - is_compressed: bool, + #[cfg(feature = "__lk-e2e-test")] + pub is_compressed: bool, /// Test-only: expose whether the byte stream was sent inline on the header packet - // #[cfg(feature = "__lk-e2e-test")] - is_inline: bool, + #[cfg(feature = "__lk-e2e-test")] + pub is_inline: bool, } /// Operation type for text streams. @@ -209,9 +209,12 @@ impl ByteStreamInfo { byte_header: proto::ByteHeader, encryption_type: EncryptionType, ) -> Self { - let is_compressed = header.compression() != proto::CompressionType::None; - let is_inline = !header.inline_content().is_empty(); Self { + #[cfg(feature = "__lk-e2e-test")] + is_compressed: header.compression() != proto::CompressionType::None, + #[cfg(feature = "__lk-e2e-test")] + is_inline: !header.inline_content().is_empty(), + id: header.stream_id, topic: header.topic, timestamp: DateTime::::from_timestamp_millis(header.timestamp) @@ -221,8 +224,6 @@ impl ByteStreamInfo { mime_type: header.mime_type, name: byte_header.name, encryption_type, - is_compressed, - is_inline, } } } @@ -237,9 +238,12 @@ impl TextStreamInfo { text_header: proto::TextHeader, encryption_type: EncryptionType, ) -> Self { - let was_compressed = header.compression() != proto::CompressionType::None; - let was_inline = !header.inline_content().is_empty(); Self { + #[cfg(feature = "__lk-e2e-test")] + is_compressed: header.compression() != proto::CompressionType::None, + #[cfg(feature = "__lk-e2e-test")] + is_inline: !header.inline_content().is_empty(), + id: header.stream_id, topic: header.topic, timestamp: DateTime::::from_timestamp_millis(header.timestamp) @@ -254,8 +258,6 @@ impl TextStreamInfo { attached_stream_ids: text_header.attached_stream_ids, generated: text_header.generated, encryption_type, - is_compressed: was_compressed, - is_inline: was_inline, } } } diff --git a/livekit/src/room/rpc/tests.rs b/livekit/src/room/rpc/tests.rs index ce40e78c6..5b61d4ef6 100644 --- a/livekit/src/room/rpc/tests.rs +++ b/livekit/src/room/rpc/tests.rs @@ -133,6 +133,10 @@ impl RpcTransport for MockTransport { attached_stream_ids: vec![], generated: false, encryption_type: EncryptionType::None, + #[cfg(feature = "__lk-e2e-test")] + is_compressed: false, + #[cfg(feature = "__lk-e2e-test")] + is_inline: false, }) } @@ -181,6 +185,10 @@ fn make_text_reader( attached_stream_ids: vec![], generated: false, encryption_type: EncryptionType::None, + #[cfg(feature = "__lk-e2e-test")] + is_compressed: false, + #[cfg(feature = "__lk-e2e-test")] + is_inline: false, }, rx, ) From ea5514f8806273450984621f5255d9e3c0f79d8f Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 17:03:57 -0400 Subject: [PATCH 24/37] feat: get random bytes e2e test to pass by using much higher quality random data --- Cargo.lock | 1 + livekit/Cargo.toml | 1 + livekit/src/room/data_stream/outgoing.rs | 20 ++++++++------------ livekit/tests/data_stream_test.rs | 12 ++++++++---- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f40be2cd3..0a66758bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3837,6 +3837,7 @@ dependencies = [ "log", "parking_lot", "prost 0.12.6", + "rand 0.9.3", "semver", "serde", "serde_json", diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index e8569618b..4f20882a9 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -61,3 +61,4 @@ test-log = "0.2.18" test-case = "3.3" serial_test = "3.0" http = "1.1" +rand = { workspace = true } diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index bbf7e1f77..030f22650 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -406,7 +406,7 @@ impl OutgoingStreamManager { // 1. Inline single-packet attempt (no attachments; all recipients are v2). if eligibility.inline && options.attached_stream_ids.is_empty() { - let (content, compression) = maybe_compress_inline(payload, compress_ok); + let (content, compression) = maybe_compress(payload, compress_ok); let (header, text_header) = build_text_header( &options, stream_id.clone(), @@ -476,7 +476,7 @@ impl OutgoingStreamManager { // 1. Inline single-packet attempt (all recipients are v2). if eligibility.inline { - let (content, compression) = maybe_compress_inline(bytes, compress_ok); + let (content, compression) = maybe_compress(bytes, compress_ok); let (header, byte_header) = build_byte_header( &options, stream_id.clone(), @@ -492,9 +492,9 @@ impl OutgoingStreamManager { } } - // 2/3. Chunked, compressed when eligible else uncompressed. - let compression = - if compress_ok { CompressionType::DeflateRaw } else { CompressionType::None }; + // 2/3. Chunked. Compress only when eligible AND it actually shrinks the payload. + // Decide compression before building the header so its flag matches what is written. + let (payload, compression) = maybe_compress(bytes, compress_ok); let (header, byte_header) = build_byte_header(&options, stream_id, name, Some(total_length), None, compression); enforce_header_size(&header, &dests)?; @@ -506,11 +506,7 @@ impl OutgoingStreamManager { }; let info = ByteStreamInfo::from_headers(header, byte_header); let mut stream = RawStream::open(open_options).await?; - if compress_ok { - stream.write_raw_chunks(&deflate_raw(bytes)).await?; - } else { - stream.write_raw_chunks(bytes).await?; - } + stream.write_raw_chunks(&payload).await?; stream.close(None).await?; Ok(info) } @@ -585,9 +581,9 @@ fn evaluate_eligibility( SendEligibility { inline, compression } } -/// Returns the inline payload and its compression flag: deflate-raw compressed when +/// Returns the payload and its compression flag: deflate-raw compressed when /// `compress` is set AND the compressed form is actually smaller, else the raw bytes. -fn maybe_compress_inline(payload: &[u8], compress: bool) -> (Vec, CompressionType) { +fn maybe_compress(payload: &[u8], compress: bool) -> (Vec, CompressionType) { if compress { let compressed = deflate_raw(payload); if compressed.len() < payload.len() { diff --git a/livekit/tests/data_stream_test.rs b/livekit/tests/data_stream_test.rs index 12b1daf75..ccdde1ed0 100644 --- a/livekit/tests/data_stream_test.rs +++ b/livekit/tests/data_stream_test.rs @@ -17,8 +17,8 @@ use { crate::common::test_rooms, anyhow::{anyhow, Ok, Result}, chrono::{TimeDelta, Utc}, - libwebrtc::native::create_random_uuid, livekit::{RoomEvent, StreamByteOptions, StreamReader, StreamTextOptions}, + rand::{rngs::StdRng, RngCore, SeedableRng}, std::time::Duration, tokio::{time::timeout, try_join}, }; @@ -47,7 +47,7 @@ async fn test_send_bytes() -> Result<()> { assert!(stream_info.total_length.is_some()); assert_eq!(stream_info.mime_type, "application/octet-stream"); assert_eq!(stream_info.topic, "some-topic"); - assert_eq!(stream_info.is_compressed, false); + assert_eq!(stream_info.is_compressed, true); assert_eq!(stream_info.is_inline, true); Ok(()) @@ -125,7 +125,11 @@ async fn test_send_large_incompressible_random_bytes() -> Result<()> { let (sending_room, _) = rooms.pop().unwrap(); let (_, mut receiving_event_rx) = rooms.pop().unwrap(); - let payload: Vec = (0..50_000).flat_map(|_| create_random_uuid().into_bytes()).collect(); + // Uniform random bytes are genuinely incompressible: deflate cannot shrink them, so the + // send path must choose CompressionType::None. Seeded for a deterministic, reproducible test. + let mut rng = StdRng::seed_from_u64(0xC0FFEE); + let mut payload = vec![0u8; 1_800_000]; + rng.fill_bytes(&mut payload); let expected = payload.clone(); let send = async move { @@ -167,7 +171,7 @@ async fn test_send_large_bytes() -> Result<()> { let options = StreamByteOptions { topic: "some-topic".into(), ..Default::default() }; let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; assert_eq!(stream_info.is_compressed, true); - assert_eq!(stream_info.is_inline, false); + assert_eq!(stream_info.is_inline, true); Ok(()) }; let receive = async move { From 7495a24fe387574fa4821c2c67710ea2e31991dc Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Wed, 1 Jul 2026 17:07:38 -0400 Subject: [PATCH 25/37] fix: comment out part of old example code --- examples/wgpu_room/src/data_streams_ui.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/wgpu_room/src/data_streams_ui.rs b/examples/wgpu_room/src/data_streams_ui.rs index 4693d1798..393da5562 100644 --- a/examples/wgpu_room/src/data_streams_ui.rs +++ b/examples/wgpu_room/src/data_streams_ui.rs @@ -120,8 +120,8 @@ impl DataStreamsUiState { return; }; service.runtime().spawn(async move { - let compressed = reader.info().compressed; - let inline = reader.info().inline; + let compressed = false; // FIXME: remove this reader.info().compressed; + let inline = false; // FIXME: remove this reader.info().inline; let (size, preview) = match reader.read_all().await { Ok(text) => (text.as_bytes().len(), truncate_chars(&text, PREVIEW_CHARS)), Err(e) => (0, format!("", e)), @@ -145,8 +145,8 @@ impl DataStreamsUiState { return; }; service.runtime().spawn(async move { - let compressed = reader.info().compressed; - let inline = reader.info().inline; + let compressed = false; // FIXME: remove this reader.info().compressed; + let inline = false; // FIXME: remove this reader.info().inline; let (size, preview) = match reader.read_all().await { Ok(data) => (data.len(), bytes_preview(data.as_ref())), Err(e) => (0, format!("", e)), From 8b7ec4d9e23e014ceff601c4108ac4a0af03481e Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 12:11:30 -0400 Subject: [PATCH 26/37] refactor: move RemoteParticipantRegistry to participant/registry.rs --- livekit/src/room/data_stream/outgoing.rs | 5 ++-- livekit/src/room/mod.rs | 2 +- livekit/src/room/participant/mod.rs | 3 +++ livekit/src/room/participant/registry.rs | 32 ++++++++++++++++++++++++ livekit/src/room/rpc/mod.rs | 18 +------------ livekit/src/room/rpc/tests.rs | 2 +- 6 files changed, 41 insertions(+), 21 deletions(-) create mode 100644 livekit/src/room/participant/registry.rs diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index 030f22650..f3cb2d5d2 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -16,8 +16,9 @@ use super::{ ByteStreamInfo, OperationType, StreamError, StreamProgress, StreamResult, TextStreamInfo, }; use crate::{ - id::ParticipantIdentity, room::participant::ClientCapability, - room::rpc::RemoteParticipantRegistry, rtc_engine::EngineError, + id::ParticipantIdentity, + room::participant::{ClientCapability, RemoteParticipantRegistry}, + rtc_engine::EngineError, utils::utf8_chunk::Utf8AwareChunkExt, }; use bmrng::unbounded::{UnboundedRequestReceiver, UnboundedRequestSender}; diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index fdabad527..7e5a8485a 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -2244,7 +2244,7 @@ impl RoomSession { } } -impl rpc::RemoteParticipantRegistry for RoomSession { +impl participant::RemoteParticipantRegistry for RoomSession { fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { self.get_remote_client_protocol(identity) } diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index ef049845f..53557bedd 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -21,6 +21,7 @@ use parking_lot::{Mutex, RwLock}; use crate::{prelude::*, rtc_engine::RtcEngine}; mod local_participant; +mod registry; mod remote_participant; mod rpc; @@ -28,6 +29,8 @@ pub use local_participant::*; pub use remote_participant::*; pub use rpc::*; +pub(crate) use registry::RemoteParticipantRegistry; + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ConnectionQuality { Excellent, diff --git a/livekit/src/room/participant/registry.rs b/livekit/src/room/participant/registry.rs new file mode 100644 index 000000000..c240556ed --- /dev/null +++ b/livekit/src/room/participant/registry.rs @@ -0,0 +1,32 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::ClientCapability; +use crate::room::id::ParticipantIdentity; + +/// Read access to remote participants' advertised protocol and capabilities. +/// +/// Shared by the RPC transport (v1/v2 transport selection) and the data-stream send +/// path (inline / compression eligibility), so both consult a single abstraction over +/// the room's remote participants and both are unit-testable with a fake. +pub(crate) trait RemoteParticipantRegistry: Send + Sync { + /// A remote participant's `client_protocol`, or `CLIENT_PROTOCOL_DEFAULT` (0) if unknown. + fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32; + + /// A remote participant's advertised capabilities, or empty if unknown. + fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec; + + /// The identities of every remote participant, used to resolve a broadcast send. + fn remote_identities(&self) -> Vec; +} diff --git a/livekit/src/room/rpc/mod.rs b/livekit/src/room/rpc/mod.rs index 2202163c3..c6d0b409b 100644 --- a/livekit/src/room/rpc/mod.rs +++ b/livekit/src/room/rpc/mod.rs @@ -23,7 +23,7 @@ pub use server::{HandleRequestOptions, RpcServerManager}; use crate::data_stream::{StreamResult, StreamTextOptions, TextStreamInfo}; use crate::room::id::ParticipantIdentity; -use crate::room::participant::ClientCapability; +use crate::room::participant::{ClientCapability, RemoteParticipantRegistry}; use livekit_protocol::RpcError as RpcError_Proto; use std::{error::Error, fmt::Display, future::Future, time::Duration}; @@ -42,22 +42,6 @@ pub(crate) const ATTR_METHOD: &str = "lk.rpc_request_method"; pub(crate) const ATTR_RESPONSE_TIMEOUT_MS: &str = "lk.rpc_request_response_timeout_ms"; pub(crate) const ATTR_VERSION: &str = "lk.rpc_request_version"; -/// Read access to remote participants' advertised protocol and capabilities. -/// -/// Shared by the RPC transport (v1/v2 transport selection) and the data-stream send -/// path (inline / compression eligibility), so both consult a single abstraction over -/// the room's remote participants and both are unit-testable with a fake. -pub(crate) trait RemoteParticipantRegistry: Send + Sync { - /// A remote participant's `client_protocol`, or `CLIENT_PROTOCOL_DEFAULT` (0) if unknown. - fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32; - - /// A remote participant's advertised capabilities, or empty if unknown. - fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec; - - /// The identities of every remote participant, used to resolve a broadcast send. - fn remote_identities(&self) -> Vec; -} - /// Transport abstraction for RPC operations. /// /// Decouples the RPC managers from concrete engine/session types, diff --git a/livekit/src/room/rpc/tests.rs b/livekit/src/room/rpc/tests.rs index 5b61d4ef6..1dfd8052f 100644 --- a/livekit/src/room/rpc/tests.rs +++ b/livekit/src/room/rpc/tests.rs @@ -18,7 +18,7 @@ use crate::data_stream::{ }; use crate::e2ee::EncryptionType; use crate::room::id::ParticipantIdentity; -use crate::room::participant::ClientCapability; +use crate::room::participant::{ClientCapability, RemoteParticipantRegistry}; use crate::room::RoomError; use bytes::Bytes; use chrono::Utc; From 54b160ca0dd277f2b68cc7f0676d9ca810c9d51b Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 13:09:20 -0400 Subject: [PATCH 27/37] refactor: restructure send_text / send_bytes Add MaybeCompressed so that compression only ever happens at most once even if it has to happen speculatively sometimes --- livekit/src/room/data_stream/outgoing.rs | 161 +++++++++++++++-------- 1 file changed, 104 insertions(+), 57 deletions(-) diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index f3cb2d5d2..ab37916d4 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -395,28 +395,40 @@ impl OutgoingStreamManager { &self, text: &str, options: StreamTextOptions, - registry: &dyn RemoteParticipantRegistry, + remote_participant_registry: &dyn RemoteParticipantRegistry, ) -> StreamResult { let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); let total_length = text.len() as u64; - let payload = text.as_bytes(); - let dests = options.destination_identities.clone(); + let mut payload = MaybeCompressed::new(text.as_bytes()); - let eligibility = evaluate_eligibility(registry, &dests); - let compress_ok = options.compress.unwrap_or(true) && eligibility.compression; + let eligibility = + evaluate_eligibility(remote_participant_registry, &options.destination_identities); + let should_compress = options.compress.unwrap_or(true) && eligibility.compression; // 1. Inline single-packet attempt (no attachments; all recipients are v2). if eligibility.inline && options.attached_stream_ids.is_empty() { - let (content, compression) = maybe_compress(payload, compress_ok); - let (header, text_header) = build_text_header( - &options, - stream_id.clone(), - Some(total_length), - Some(content), - compression, - ); - if header_packet_fits(&header, &dests) { - let packet = RawStream::create_header_packet(header.clone(), dests); + let (header, text_header) = + if should_compress && payload.as_compressed()?.len() < payload.uncompressed.len() { + build_text_header( + &options, + stream_id.clone(), + Some(total_length), + Some(payload.as_compressed()?.to_vec()), + CompressionType::DeflateRaw, + ) + } else { + build_text_header( + &options, + stream_id.clone(), + Some(total_length), + Some(payload.uncompressed.to_vec()), + CompressionType::None, + ) + }; + + if header_packet_fits(&header, &options.destination_identities) { + let packet = + RawStream::create_header_packet(header.clone(), options.destination_identities); RawStream::send_packet(&self.packet_tx, packet).await?; return Ok(TextStreamInfo::from_headers(header, text_header)); } @@ -425,22 +437,22 @@ impl OutgoingStreamManager { // 2/3. Chunked, compressed when eligible else uncompressed. let compression = - if compress_ok { CompressionType::DeflateRaw } else { CompressionType::None }; + if should_compress { CompressionType::DeflateRaw } else { CompressionType::None }; let (header, text_header) = build_text_header(&options, stream_id, Some(total_length), None, compression); - enforce_header_size(&header, &dests)?; + enforce_header_size(&header, &options.destination_identities)?; let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: dests, + destination_identities: options.destination_identities, packet_tx: self.packet_tx.clone(), }; let info = TextStreamInfo::from_headers(header, text_header); let mut stream = RawStream::open(open_options).await?; - if compress_ok { - stream.write_raw_chunks(&deflate_raw(payload)).await?; + if should_compress { + stream.write_raw_chunks(payload.as_compressed()?).await?; } else { - for chunk in payload.utf8_aware_chunks(STREAM_CHUNK_SIZE_BYTES) { + for chunk in payload.uncompressed.utf8_aware_chunks(STREAM_CHUNK_SIZE_BYTES) { stream.write_chunk(chunk).await?; } } @@ -461,7 +473,7 @@ impl OutgoingStreamManager { &self, data: impl AsRef<[u8]>, options: StreamByteOptions, - registry: &dyn RemoteParticipantRegistry, + remote_participant_registry: &dyn RemoteParticipantRegistry, ) -> StreamResult { if options.total_length.is_some() { log::warn!("Ignoring total_length option specified for send_bytes"); @@ -471,21 +483,33 @@ impl OutgoingStreamManager { let name = options.name.clone().unwrap_or_else(|| BYTE_DEFAULT_NAME.to_owned()); let total_length = bytes.len() as u64; let dests = options.destination_identities.clone(); + let mut payload = MaybeCompressed::new(bytes); - let eligibility = evaluate_eligibility(registry, &dests); - let compress_ok = options.compress.unwrap_or(true) && eligibility.compression; + let eligibility = evaluate_eligibility(remote_participant_registry, &dests); + let should_compress = options.compress.unwrap_or(true) && eligibility.compression; // 1. Inline single-packet attempt (all recipients are v2). if eligibility.inline { - let (content, compression) = maybe_compress(bytes, compress_ok); - let (header, byte_header) = build_byte_header( - &options, - stream_id.clone(), - name.clone(), - Some(total_length), - Some(content), - compression, - ); + let (header, byte_header) = + if should_compress && payload.as_compressed()?.len() < payload.uncompressed.len() { + build_byte_header( + &options, + stream_id.clone(), + name.clone(), + Some(total_length), + Some(payload.as_compressed()?.to_vec()), + CompressionType::DeflateRaw, + ) + } else { + build_byte_header( + &options, + stream_id.clone(), + name.clone(), + Some(total_length), + Some(payload.uncompressed.to_vec()), + CompressionType::None, + ) + }; if header_packet_fits(&header, &dests) { let packet = RawStream::create_header_packet(header.clone(), dests); RawStream::send_packet(&self.packet_tx, packet).await?; @@ -493,9 +517,9 @@ impl OutgoingStreamManager { } } - // 2/3. Chunked. Compress only when eligible AND it actually shrinks the payload. - // Decide compression before building the header so its flag matches what is written. - let (payload, compression) = maybe_compress(bytes, compress_ok); + // 2/3. Chunked, compressed when eligible else uncompressed. + let compression = + if should_compress { CompressionType::DeflateRaw } else { CompressionType::None }; let (header, byte_header) = build_byte_header(&options, stream_id, name, Some(total_length), None, compression); enforce_header_size(&header, &dests)?; @@ -507,7 +531,11 @@ impl OutgoingStreamManager { }; let info = ByteStreamInfo::from_headers(header, byte_header); let mut stream = RawStream::open(open_options).await?; - stream.write_raw_chunks(&payload).await?; + if should_compress { + stream.write_raw_chunks(payload.as_compressed()?).await?; + } else { + stream.write_raw_chunks(payload.uncompressed).await?; + } stream.close(None).await?; Ok(info) } @@ -521,7 +549,7 @@ impl OutgoingStreamManager { &self, path: impl AsRef, options: StreamByteOptions, - registry: &dyn RemoteParticipantRegistry, + remote_participant_registry: &dyn RemoteParticipantRegistry, ) -> StreamResult { let path = path.as_ref(); let file_size = tokio::fs::metadata(path) @@ -532,10 +560,10 @@ impl OutgoingStreamManager { let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); let dests = options.destination_identities.clone(); - let eligibility = evaluate_eligibility(registry, &dests); - let compress_ok = options.compress.unwrap_or(true) && eligibility.compression; + let eligibility = evaluate_eligibility(remote_participant_registry, &dests); + let should_compress = options.compress.unwrap_or(true) && eligibility.compression; let compression = - if compress_ok { CompressionType::DeflateRaw } else { CompressionType::None }; + if should_compress { CompressionType::DeflateRaw } else { CompressionType::None }; let (header, byte_header) = build_byte_header(&options, stream_id, name, Some(file_size), None, compression); @@ -548,7 +576,7 @@ impl OutgoingStreamManager { }; let info = ByteStreamInfo::from_headers(header, byte_header); let mut stream = RawStream::open(open_options).await?; - stream.write_file(path, compress_ok).await?; + stream.write_file(path, should_compress).await?; stream.close(None).await?; Ok(info) } @@ -579,27 +607,46 @@ fn evaluate_eligibility( && recipients.iter().all(|id| { registry.remote_capabilities(id).contains(&ClientCapability::CompressionDeflateRaw) }); + SendEligibility { inline, compression } } -/// Returns the payload and its compression flag: deflate-raw compressed when -/// `compress` is set AND the compressed form is actually smaller, else the raw bytes. -fn maybe_compress(payload: &[u8], compress: bool) -> (Vec, CompressionType) { - if compress { - let compressed = deflate_raw(payload); - if compressed.len() < payload.len() { - return (compressed, CompressionType::DeflateRaw); +struct MaybeCompressed<'a> { + uncompressed: &'a [u8], + compressed: Option>, +} + +impl<'a> MaybeCompressed<'a> { + fn new(uncompressed: &'a [u8]) -> Self { + Self { uncompressed, compressed: None } + } + + /// Upconverts the Uncompressed variant into the Compressed variant, and returns a reference to + /// the compressed bytes as a result. + fn as_compressed(&mut self) -> Result<&[u8], std::io::Error> { + match &mut self.compressed { + Some(compressed) => Ok(&*compressed), + compressed_option @ None => { + let mut encoder = + flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default()); + encoder.write_all(self.uncompressed)?; + *compressed_option = Some(encoder.finish()?); + let Some(ref data) = compressed_option else { + unreachable!("compressed data just set") + }; + Ok(data) + } } } - (payload.to_vec(), CompressionType::None) -} -/// One-shot deflate-raw (raw DEFLATE, no zlib/gzip wrapper) of the full payload. -fn deflate_raw(data: &[u8]) -> Vec { - let mut encoder = - flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default()); - encoder.write_all(data).expect("deflate write to Vec is infallible"); - encoder.finish().expect("deflate finish into Vec is infallible") + // /// Consumes and converts into compressed bytes + // fn into_compressed(mut self) -> Result, std::io::Error> { + // self.as_compressed()?; + // let Some(data) = self.compressed else { + // unreachable!("compressed data should be set in .as_compressed()"); + // }; + // Ok(data) + // } } /// Whether the serialized header `DataPacket` fits within the MTU budget. From ad6f7c83901ecee287e944d90353a4d2b6f3d34f Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 13:47:18 -0400 Subject: [PATCH 28/37] feat: throw error when remove participant disconnects halfway through sending a data stream This matches with how web does it --- livekit/src/room/data_stream/incoming.rs | 60 +++++++++++++++++++++++- livekit/src/room/mod.rs | 6 +++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index b1fc5f525..0eb6b7a0c 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -237,6 +237,9 @@ struct Descriptor { progress: StreamProgress, chunk_tx: UnboundedSender>, encryption_type: EncryptionType, + /// Identity of the participant sending this stream; used to abort the stream + /// if that participant disconnects mid-send. + sender_identity: String, is_internal: bool, /// Whether this is a text stream (decompressed output is reframed on UTF-8 boundaries). is_text: bool, @@ -355,7 +358,7 @@ impl IncomingStreamManager { } let (reader, chunk_tx) = AnyStreamReader::from(info); - let _ = self.open_tx.send((reader, identity)); + let _ = self.open_tx.send((reader, identity.clone())); // Inline single-packet stream: synthesize the complete content now; no chunk/trailer // packets will follow, so we never register an open descriptor. @@ -385,6 +388,7 @@ impl IncomingStreamManager { progress: StreamProgress { bytes_total, ..Default::default() }, chunk_tx, encryption_type: stream_encryption_type, + sender_identity: identity, is_internal, is_text, decompressor: is_compressed.then(DeflateDecompressState::new), @@ -517,6 +521,29 @@ impl IncomingStreamManager { } inner.close_stream(&id); } + + /// Aborts every open stream being sent by the given participant, erroring each + /// reader with [`StreamError::AbnormalEnd`]. + /// + /// Called when a remote participant disconnects: any streams it had in flight to + /// this receiver are terminated so their readers observe an error rather than + /// hanging forever waiting for chunks that will never arrive. + pub fn abort_streams_from(&self, identity: &str) { + let mut inner = self.inner.lock(); + let ids: Vec = inner + .open_streams + .iter() + .filter(|(_, descriptor)| descriptor.sender_identity == identity) + .map(|(id, _)| id.clone()) + .collect(); + for id in ids { + let reason = format!( + "Participant {} unexpectedly disconnected in the middle of sending data", + identity + ); + inner.close_stream_with_error(&id, StreamError::AbnormalEnd(reason)); + } + } } impl ManagerInner { @@ -875,6 +902,37 @@ mod tests { assert_eq!(read_text(reader).await.unwrap(), text); } + #[tokio::test] + async fn errors_open_streams_on_sender_disconnect() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + text_header("s1", Some(10), HashMap::new(), None, proto::CompressionType::None), + SENDER.to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + // Partial content, no trailer: the sender then drops. + mgr.handle_chunk(chunk("s1", 0, vec![b'h', b'e', b'l', b'l', b'o']), EncType::None); + mgr.abort_streams_from(SENDER); + assert!(matches!(read_text(reader).await, Err(StreamError::AbnormalEnd(_)))); + } + + #[tokio::test] + async fn abort_only_affects_matching_sender() { + let (mgr, mut rx) = IncomingStreamManager::new(); + mgr.handle_header( + text_header("s1", Some(5), HashMap::new(), None, proto::CompressionType::None), + "bob".to_string(), + EncType::None, + ); + let (reader, _) = recv_reader(&mut rx).await; + mgr.handle_chunk(chunk("s1", 0, vec![b'h', b'e', b'l', b'l', b'o']), EncType::None); + // A different participant disconnecting must not disturb bob's stream. + mgr.abort_streams_from(SENDER); + mgr.handle_trailer(trailer("s1")); + assert_eq!(read_text(reader).await.unwrap(), "hello"); + } + #[tokio::test] async fn v2_compressed_gap_errors() { let (mgr, mut rx) = IncomingStreamManager::new(); diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index 7e5a8485a..e4e3eface 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -2154,6 +2154,12 @@ impl RoomSession { let mut participants = self.remote_participants.write(); participants.remove(&remote_participant.identity()); + drop(participants); + + // Terminate any data streams this participant was still sending; otherwise their + // readers would hang waiting for chunks that will never arrive. + self.incoming_stream_manager.abort_streams_from(remote_participant.identity().as_str()); + self.dispatcher.dispatch(&RoomEvent::ParticipantDisconnected(remote_participant)); } From 6ecf774a94f79f15036e462001568a022cab5517 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 15:46:11 -0400 Subject: [PATCH 29/37] fix: adjust send_text / send_bytes to make the disambiguation between can_compress / should_compress more clear --- livekit/src/room/data_stream/outgoing.rs | 147 +++++++++++------------ 1 file changed, 67 insertions(+), 80 deletions(-) diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index ab37916d4..af07971a8 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -403,45 +403,43 @@ impl OutgoingStreamManager { let eligibility = evaluate_eligibility(remote_participant_registry, &options.destination_identities); - let should_compress = options.compress.unwrap_or(true) && eligibility.compression; - - // 1. Inline single-packet attempt (no attachments; all recipients are v2). - if eligibility.inline && options.attached_stream_ids.is_empty() { - let (header, text_header) = - if should_compress && payload.as_compressed()?.len() < payload.uncompressed.len() { - build_text_header( - &options, - stream_id.clone(), - Some(total_length), - Some(payload.as_compressed()?.to_vec()), - CompressionType::DeflateRaw, - ) - } else { - build_text_header( - &options, - stream_id.clone(), - Some(total_length), - Some(payload.uncompressed.to_vec()), - CompressionType::None, - ) - }; - - if header_packet_fits(&header, &options.destination_identities) { - let packet = - RawStream::create_header_packet(header.clone(), options.destination_identities); - RawStream::send_packet(&self.packet_tx, packet).await?; - return Ok(TextStreamInfo::from_headers(header, text_header)); - } - // Otherwise (large payload), fall through to the chunked path. + let can_compress = options.compress.unwrap_or(true) && eligibility.compression; + + // 1. Inline single-packet attempt (no attachments; all recipients are >= v2). + let (mut header, text_header) = + if can_compress && payload.as_compressed()?.len() < payload.uncompressed.len() { + build_text_header( + &options, + stream_id.clone(), + Some(total_length), + Some(payload.as_compressed()?.to_vec()), + CompressionType::DeflateRaw, + ) + } else { + build_text_header( + &options, + stream_id.clone(), + Some(total_length), + Some(payload.uncompressed.to_vec()), + CompressionType::None, + ) + }; + if eligibility.inline + && options.attached_stream_ids.is_empty() + && header_packet_fits(&header, &options.destination_identities) + { + let packet = + RawStream::create_header_packet(header.clone(), options.destination_identities); + RawStream::send_packet(&self.packet_tx, packet).await?; + return Ok(TextStreamInfo::from_headers(header, text_header)); } // 2/3. Chunked, compressed when eligible else uncompressed. - let compression = - if should_compress { CompressionType::DeflateRaw } else { CompressionType::None }; - let (header, text_header) = - build_text_header(&options, stream_id, Some(total_length), None, compression); + header.inline_content = None; enforce_header_size(&header, &options.destination_identities)?; + let should_compress = + header.compression() == proto::data_stream::CompressionType::DeflateRaw; let open_options = RawStreamOpenOptions { header: header.clone(), destination_identities: options.destination_identities, @@ -482,51 +480,49 @@ impl OutgoingStreamManager { let stream_id = options.id.clone().unwrap_or_else(create_random_uuid); let name = options.name.clone().unwrap_or_else(|| BYTE_DEFAULT_NAME.to_owned()); let total_length = bytes.len() as u64; - let dests = options.destination_identities.clone(); let mut payload = MaybeCompressed::new(bytes); - let eligibility = evaluate_eligibility(remote_participant_registry, &dests); - let should_compress = options.compress.unwrap_or(true) && eligibility.compression; - - // 1. Inline single-packet attempt (all recipients are v2). - if eligibility.inline { - let (header, byte_header) = - if should_compress && payload.as_compressed()?.len() < payload.uncompressed.len() { - build_byte_header( - &options, - stream_id.clone(), - name.clone(), - Some(total_length), - Some(payload.as_compressed()?.to_vec()), - CompressionType::DeflateRaw, - ) - } else { - build_byte_header( - &options, - stream_id.clone(), - name.clone(), - Some(total_length), - Some(payload.uncompressed.to_vec()), - CompressionType::None, - ) - }; - if header_packet_fits(&header, &dests) { - let packet = RawStream::create_header_packet(header.clone(), dests); - RawStream::send_packet(&self.packet_tx, packet).await?; - return Ok(ByteStreamInfo::from_headers(header, byte_header)); - } + let eligibility = + evaluate_eligibility(remote_participant_registry, &options.destination_identities); + let can_compress = options.compress.unwrap_or(true) && eligibility.compression; + + // 1. Inline single-packet attempt (if all recipients are >= v2). + let (mut header, byte_header) = + if can_compress && payload.as_compressed()?.len() < payload.uncompressed.len() { + build_byte_header( + &options, + stream_id.clone(), + name.clone(), + Some(total_length), // NOTE: this is purposely always uncompressed length + Some(payload.as_compressed()?.to_vec()), + CompressionType::DeflateRaw, + ) + } else { + build_byte_header( + &options, + stream_id.clone(), + name.clone(), + Some(total_length), // NOTE: this is purposely always uncompressed length + Some(payload.uncompressed.to_vec()), + CompressionType::None, + ) + }; + if eligibility.inline && header_packet_fits(&header, &options.destination_identities) { + let packet = + RawStream::create_header_packet(header.clone(), options.destination_identities); + RawStream::send_packet(&self.packet_tx, packet).await?; + return Ok(ByteStreamInfo::from_headers(header, byte_header)); } // 2/3. Chunked, compressed when eligible else uncompressed. - let compression = - if should_compress { CompressionType::DeflateRaw } else { CompressionType::None }; - let (header, byte_header) = - build_byte_header(&options, stream_id, name, Some(total_length), None, compression); - enforce_header_size(&header, &dests)?; + header.inline_content = None; + enforce_header_size(&header, &options.destination_identities)?; + let should_compress = + header.compression() == proto::data_stream::CompressionType::DeflateRaw; let open_options = RawStreamOpenOptions { header: header.clone(), - destination_identities: dests, + destination_identities: options.destination_identities, packet_tx: self.packet_tx.clone(), }; let info = ByteStreamInfo::from_headers(header, byte_header); @@ -638,15 +634,6 @@ impl<'a> MaybeCompressed<'a> { } } } - - // /// Consumes and converts into compressed bytes - // fn into_compressed(mut self) -> Result, std::io::Error> { - // self.as_compressed()?; - // let Some(data) = self.compressed else { - // unreachable!("compressed data should be set in .as_compressed()"); - // }; - // Ok(data) - // } } /// Whether the serialized header `DataPacket` fits within the MTU budget. From fd8d497fbbe82b8ff5a71756d73ad323a2a9668f Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 15:52:35 -0400 Subject: [PATCH 30/37] fix: clean up incoming stream manager --- livekit/src/room/data_stream/incoming.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index 0eb6b7a0c..dc8b27fbb 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -243,7 +243,7 @@ struct Descriptor { is_internal: bool, /// Whether this is a text stream (decompressed output is reframed on UTF-8 boundaries). is_text: bool, - /// Per-stream deflate-raw decompressor; `Some` iff the header declared `DEFLATE_RAW`. + /// Per-stream deflate-raw decompressor; `Some` if the header declared `DEFLATE_RAW`. decompressor: Option, /// Highest chunk index processed so far (compressed streams; for dedup/gap detection). last_chunk_index: Option, @@ -265,9 +265,9 @@ impl DeflateDecompressState { /// Feeds compressed `input` through the stateful decompressor, returning all /// decompressed output produced so far. - fn run(&mut self, input: &[u8]) -> StreamResult> { + fn push(&mut self, input: &[u8]) -> StreamResult> { let mut out = Vec::new(); - let mut buf = vec![0u8; 16384]; + let mut buf = vec![]; let mut offset = 0; loop { let in_before = self.decompress.total_in(); @@ -445,7 +445,7 @@ impl IncomingStreamManager { let is_text = descriptor.is_text; // Confine the decompressor borrow so we can re-borrow `inner` afterwards. let result: StreamResult<(u64, Bytes)> = { - match decompressor.run(&chunk.content) { + match decompressor.push(&chunk.content) { Ok(decompressed) => { let produced = decompressed.len() as u64; let yielded = if is_text { From 7ea47dbf68407fb32fe22866028b52339744af00 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 16:05:11 -0400 Subject: [PATCH 31/37] fix: preload buffer length --- livekit/src/room/data_stream/incoming.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/livekit/src/room/data_stream/incoming.rs b/livekit/src/room/data_stream/incoming.rs index dc8b27fbb..1218a012a 100644 --- a/livekit/src/room/data_stream/incoming.rs +++ b/livekit/src/room/data_stream/incoming.rs @@ -267,7 +267,7 @@ impl DeflateDecompressState { /// decompressed output produced so far. fn push(&mut self, input: &[u8]) -> StreamResult> { let mut out = Vec::new(); - let mut buf = vec![]; + let mut buf = vec![0u8; 16384 /* number of bytes to process every loop iteration */]; let mut offset = 0; loop { let in_before = self.decompress.total_in(); From 0523bdf578b456ca209021879395c2eb4a22a678 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 16:06:19 -0400 Subject: [PATCH 32/37] fix: add reason labels on e2e test assertions --- livekit/tests/data_stream_test.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/livekit/tests/data_stream_test.rs b/livekit/tests/data_stream_test.rs index ccdde1ed0..882bd24a6 100644 --- a/livekit/tests/data_stream_test.rs +++ b/livekit/tests/data_stream_test.rs @@ -135,8 +135,8 @@ async fn test_send_large_incompressible_random_bytes() -> Result<()> { let send = async move { let options = StreamByteOptions { topic: "some-topic".into(), ..Default::default() }; let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; - assert_eq!(stream_info.is_compressed, false); - assert_eq!(stream_info.is_inline, false); + assert_eq!(stream_info.is_compressed, false, "is_compressed was not false"); + assert_eq!(stream_info.is_inline, false, "is_inline was not false"); Ok(()) }; let receive = async move { @@ -170,8 +170,8 @@ async fn test_send_large_bytes() -> Result<()> { let send = async move { let options = StreamByteOptions { topic: "some-topic".into(), ..Default::default() }; let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; - assert_eq!(stream_info.is_compressed, true); - assert_eq!(stream_info.is_inline, true); + assert_eq!(stream_info.is_compressed, true, "is_compressed was not true"); + assert_eq!(stream_info.is_inline, true, "is_inline was not true"); Ok(()) }; let receive = async move { @@ -210,8 +210,8 @@ async fn test_data_stream_compress_false() -> Result<()> { ..Default::default() }; let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?; - assert_eq!(stream_info.is_compressed, false); - assert_eq!(stream_info.is_inline, false); + assert_eq!(stream_info.is_compressed, false, "is_compressed was not false"); + assert_eq!(stream_info.is_inline, false, "is_inline was not false"); Ok(()) }; let receive = async move { From 3e24ce3a2d0338d448d59c21333574626fc416b5 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 17:05:57 -0400 Subject: [PATCH 33/37] feat: add initial livekit-common implementation --- livekit-common/Cargo.toml | 12 +++ livekit-common/README.md | 8 ++ livekit-common/src/lib.rs | 178 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+) create mode 100644 livekit-common/Cargo.toml create mode 100644 livekit-common/README.md create mode 100644 livekit-common/src/lib.rs diff --git a/livekit-common/Cargo.toml b/livekit-common/Cargo.toml new file mode 100644 index 000000000..0b83ade48 --- /dev/null +++ b/livekit-common/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "livekit-common" +description = "Common foundational types shared across LiveKit crates" +version = "0.1.0" +readme = "README.md" +license.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +livekit-protocol = { workspace = true } +serde = { workspace = true, features = ["derive"] } diff --git a/livekit-common/README.md b/livekit-common/README.md new file mode 100644 index 000000000..17f6cf712 --- /dev/null +++ b/livekit-common/README.md @@ -0,0 +1,8 @@ +# LiveKit Common + +**Important**: +This is an internal crate holding foundational types shared across the LiveKit client SDK crates +(identities, encryption/capability enums, client-protocol constants, and the remote-participant +registry trait). It is not intended to be used directly. + +To build applications with LiveKit, please use the public APIs provided by the client SDKs. diff --git a/livekit-common/src/lib.rs b/livekit-common/src/lib.rs new file mode 100644 index 000000000..69a11de77 --- /dev/null +++ b/livekit-common/src/lib.rs @@ -0,0 +1,178 @@ +// Copyright 2026 LiveKit, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Foundational types shared across LiveKit crates: participant identities, the +//! encryption/capability enums, client-protocol constants, and the remote-participant +//! registry trait consulted by the data-stream and RPC send paths. + +use std::fmt::Display; + +use livekit_protocol as proto; +use serde::{Deserialize, Serialize}; + +// ------------------------------------------------------------------------------------------------- +// Client protocol +// ------------------------------------------------------------------------------------------------- + +/// Legacy client. No v2 data-stream features. +pub const CLIENT_PROTOCOL_DEFAULT: i32 = 0; + +/// RPC v2 (see RPC spec). No v2 data-stream features. +pub const CLIENT_PROTOCOL_DATA_STREAM_RPC: i32 = 1; + +/// Understands inline single-packet data streams (data streams v2). +pub const CLIENT_PROTOCOL_DATA_STREAM_V2: i32 = 2; + +// ------------------------------------------------------------------------------------------------- +// ParticipantIdentity +// ------------------------------------------------------------------------------------------------- + +#[derive(Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct ParticipantIdentity(pub String); + +impl From for ParticipantIdentity { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for ParticipantIdentity { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +impl From for String { + fn from(value: ParticipantIdentity) -> Self { + value.0 + } +} + +impl Display for ParticipantIdentity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl ParticipantIdentity { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +// ------------------------------------------------------------------------------------------------- +// EncryptionType +// ------------------------------------------------------------------------------------------------- + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum EncryptionType { + #[default] + None, + Gcm, + Custom, +} + +impl From for EncryptionType { + fn from(value: proto::encryption::Type) -> Self { + match value { + proto::encryption::Type::None => Self::None, + proto::encryption::Type::Gcm => Self::Gcm, + proto::encryption::Type::Custom => Self::Custom, + } + } +} + +impl From for proto::encryption::Type { + fn from(value: EncryptionType) -> Self { + match value { + EncryptionType::None => Self::None, + EncryptionType::Gcm => Self::Gcm, + EncryptionType::Custom => Self::Custom, + } + } +} + +impl From for i32 { + fn from(value: EncryptionType) -> Self { + match value { + EncryptionType::None => 0, + EncryptionType::Gcm => 1, + EncryptionType::Custom => 2, + } + } +} + +// ------------------------------------------------------------------------------------------------- +// ClientCapability +// ------------------------------------------------------------------------------------------------- + +/// A capability a participant's client advertises (mirrors `ClientInfo.Capability`). +/// +/// Stored typed rather than as the raw protobuf `i32` so accessors don't leak protobuf types. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum ClientCapability { + Unused, + PacketTrailer, + CompressionDeflateRaw, +} + +impl TryFrom for ClientCapability { + type Error = &'static str; + + fn try_from(value: i32) -> Result { + match proto::client_info::Capability::try_from(value) { + Ok(proto::client_info::Capability::CapPacketTrailer) => Ok(Self::PacketTrailer), + Ok(proto::client_info::Capability::CapCompressionDeflateRaw) => { + Ok(Self::CompressionDeflateRaw) + } + Ok(proto::client_info::Capability::CapUnused) => Ok(Self::Unused), + Err(_) => Err("unknown client capability"), + } + } +} + +impl From for i32 { + fn from(value: ClientCapability) -> Self { + match value { + ClientCapability::Unused => proto::client_info::Capability::CapUnused as i32, + ClientCapability::PacketTrailer => { + proto::client_info::Capability::CapPacketTrailer as i32 + } + ClientCapability::CompressionDeflateRaw => { + proto::client_info::Capability::CapCompressionDeflateRaw as i32 + } + } + } +} + +// ------------------------------------------------------------------------------------------------- +// RemoteParticipantRegistry +// ------------------------------------------------------------------------------------------------- + +/// Read access to remote participants' advertised protocol and capabilities. +/// +/// Shared by the RPC transport (v1/v2 transport selection) and the data-stream send +/// path (inline / compression eligibility), so both consult a single abstraction over +/// the room's remote participants and both are unit-testable with a fake. +pub trait RemoteParticipantRegistry: Send + Sync { + /// A remote participant's `client_protocol`, or `CLIENT_PROTOCOL_DEFAULT` (0) if unknown. + fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32; + + /// A remote participant's advertised capabilities, or empty if unknown. + fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec; + + /// The identities of every remote participant, used to resolve a broadcast send. + fn remote_identities(&self) -> Vec; +} From f8c54e1af2fd14f0d097677cdd6628ad0a64c5e5 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 17:10:36 -0400 Subject: [PATCH 34/37] refactor: depend on livekit_common::RemoteParticipantRegistry --- livekit/Cargo.toml | 1 + livekit/src/room/data_stream/outgoing.rs | 3 ++- livekit/src/room/mod.rs | 2 +- livekit/src/room/participant/mod.rs | 3 --- livekit/src/room/participant/registry.rs | 32 ------------------------ livekit/src/room/rpc/mod.rs | 3 ++- livekit/src/room/rpc/tests.rs | 3 ++- 7 files changed, 8 insertions(+), 39 deletions(-) delete mode 100644 livekit/src/room/participant/registry.rs diff --git a/livekit/Cargo.toml b/livekit/Cargo.toml index 4f20882a9..1fc6e52c9 100644 --- a/livekit/Cargo.toml +++ b/livekit/Cargo.toml @@ -36,6 +36,7 @@ livekit-runtime = { workspace = true } livekit-api = { workspace = true } libwebrtc = { workspace = true } livekit-protocol = { workspace = true } +livekit-common = { workspace = true } livekit-datatrack = { workspace = true } prost = "0.12" serde = { version = "1", features = ["derive"] } diff --git a/livekit/src/room/data_stream/outgoing.rs b/livekit/src/room/data_stream/outgoing.rs index af07971a8..e94bd3242 100644 --- a/livekit/src/room/data_stream/outgoing.rs +++ b/livekit/src/room/data_stream/outgoing.rs @@ -17,7 +17,7 @@ use super::{ }; use crate::{ id::ParticipantIdentity, - room::participant::{ClientCapability, RemoteParticipantRegistry}, + room::participant::ClientCapability, rtc_engine::EngineError, utils::utf8_chunk::Utf8AwareChunkExt, }; @@ -25,6 +25,7 @@ use bmrng::unbounded::{UnboundedRequestReceiver, UnboundedRequestSender}; use chrono::Utc; use libwebrtc::native::create_random_uuid; use livekit_api::signal_client::CLIENT_PROTOCOL_DATA_STREAM_V2; +use livekit_common::RemoteParticipantRegistry; use livekit_protocol as proto; use proto::data_stream::CompressionType; use std::{collections::HashMap, io::Write, path::Path, sync::Arc}; diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index e4e3eface..f140bcc02 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -2250,7 +2250,7 @@ impl RoomSession { } } -impl participant::RemoteParticipantRegistry for RoomSession { +impl livekit_common::RemoteParticipantRegistry for RoomSession { fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32 { self.get_remote_client_protocol(identity) } diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index 53557bedd..ef049845f 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -21,7 +21,6 @@ use parking_lot::{Mutex, RwLock}; use crate::{prelude::*, rtc_engine::RtcEngine}; mod local_participant; -mod registry; mod remote_participant; mod rpc; @@ -29,8 +28,6 @@ pub use local_participant::*; pub use remote_participant::*; pub use rpc::*; -pub(crate) use registry::RemoteParticipantRegistry; - #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ConnectionQuality { Excellent, diff --git a/livekit/src/room/participant/registry.rs b/livekit/src/room/participant/registry.rs deleted file mode 100644 index c240556ed..000000000 --- a/livekit/src/room/participant/registry.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2026 LiveKit, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use super::ClientCapability; -use crate::room::id::ParticipantIdentity; - -/// Read access to remote participants' advertised protocol and capabilities. -/// -/// Shared by the RPC transport (v1/v2 transport selection) and the data-stream send -/// path (inline / compression eligibility), so both consult a single abstraction over -/// the room's remote participants and both are unit-testable with a fake. -pub(crate) trait RemoteParticipantRegistry: Send + Sync { - /// A remote participant's `client_protocol`, or `CLIENT_PROTOCOL_DEFAULT` (0) if unknown. - fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32; - - /// A remote participant's advertised capabilities, or empty if unknown. - fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec; - - /// The identities of every remote participant, used to resolve a broadcast send. - fn remote_identities(&self) -> Vec; -} diff --git a/livekit/src/room/rpc/mod.rs b/livekit/src/room/rpc/mod.rs index c6d0b409b..19c03433e 100644 --- a/livekit/src/room/rpc/mod.rs +++ b/livekit/src/room/rpc/mod.rs @@ -23,8 +23,9 @@ pub use server::{HandleRequestOptions, RpcServerManager}; use crate::data_stream::{StreamResult, StreamTextOptions, TextStreamInfo}; use crate::room::id::ParticipantIdentity; -use crate::room::participant::{ClientCapability, RemoteParticipantRegistry}; +use crate::room::participant::ClientCapability; use livekit_protocol::RpcError as RpcError_Proto; +use livekit_common::RemoteParticipantRegistry; use std::{error::Error, fmt::Display, future::Future, time::Duration}; // RPC protocol version constants (distinct from client_protocol; this is the diff --git a/livekit/src/room/rpc/tests.rs b/livekit/src/room/rpc/tests.rs index 1dfd8052f..096b963f5 100644 --- a/livekit/src/room/rpc/tests.rs +++ b/livekit/src/room/rpc/tests.rs @@ -18,12 +18,13 @@ use crate::data_stream::{ }; use crate::e2ee::EncryptionType; use crate::room::id::ParticipantIdentity; -use crate::room::participant::{ClientCapability, RemoteParticipantRegistry}; +use crate::room::participant::ClientCapability; use crate::room::RoomError; use bytes::Bytes; use chrono::Utc; use livekit_api::signal_client::{CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT}; use livekit_protocol as proto; +use livekit_common::RemoteParticipantRegistry; use parking_lot::Mutex as ParkingMutex; use std::collections::HashMap; use std::sync::Arc; From 7ecc7e61f837800e51ff6e57918c1dd6b8fba355 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 17:21:27 -0400 Subject: [PATCH 35/37] fix: remove orphaned take_if_raw method --- livekit/src/utils/take_cell.rs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/livekit/src/utils/take_cell.rs b/livekit/src/utils/take_cell.rs index c22967ea1..6f3b686ab 100644 --- a/livekit/src/utils/take_cell.rs +++ b/livekit/src/utils/take_cell.rs @@ -26,18 +26,6 @@ impl TakeCell { Self { value: Arc::new(RwLock::new(Some(value))) } } - /// Take ownership of the value in the cell if it matches some predicate. - /// - /// This method will only take the value if the provided predicate returns `true` when called with the current value. - /// If the predicate returns `false` or the value has already been taken, this method returns `None`. - pub(crate) fn take_if_raw(&self, predicate: impl FnOnce(&T) -> bool) -> Option { - if self.value.read().as_ref().map_or(false, |v| predicate(v)) { - self.take() - } else { - None - } - } - /// Take ownership of the value in the cell. If the value has, /// already been taken, the result is `None`. pub fn take(&self) -> Option { @@ -83,14 +71,6 @@ mod tests { assert_eq!(cell.is_taken(), true); } - #[test] - fn test_take_if_raw() { - let cell = TakeCell::new(1); - assert_eq!(cell.take_if_raw(|value| *value == 2), None); - assert_eq!(cell.take_if_raw(|value| *value == 1), Some(1)); - assert_eq!(cell.take_if_raw(|value| *value == 1), None); - } - #[test] fn test_debug() { let cell = TakeCell::new(1); From 783acb29b4ec8855dc85dd30cd45fe0b9d5e19e0 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 17:24:24 -0400 Subject: [PATCH 36/37] refactor: update more livekit crate references to point to livekit-common instead --- Cargo.lock | 28 +++++++++++++++++++ Cargo.toml | 2 ++ livekit-api/Cargo.toml | 1 + livekit-api/src/signal_client/mod.rs | 14 ++++------ livekit/src/proto.rs | 29 -------------------- livekit/src/room/e2ee/mod.rs | 8 +----- livekit/src/room/id.rs | 18 ++---------- livekit/src/room/mod.rs | 2 +- livekit/src/room/participant/mod.rs | 41 +--------------------------- 9 files changed, 42 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a66758bf..6cb80528b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3831,6 +3831,7 @@ dependencies = [ "libloading 0.8.9", "libwebrtc", "livekit-api", + "livekit-common", "livekit-datatrack", "livekit-protocol", "livekit-runtime", @@ -3863,6 +3864,7 @@ dependencies = [ "http 1.4.0", "isahc", "jsonwebtoken", + "livekit-common", "livekit-protocol", "livekit-runtime", "log", @@ -3885,6 +3887,32 @@ dependencies = [ "url", ] +[[package]] +name = "livekit-common" +version = "0.1.0" +dependencies = [ + "livekit-protocol", +] + +[[package]] +name = "livekit-data-stream" +version = "0.1.0" +dependencies = [ + "bmrng", + "bytes", + "chrono", + "flate2", + "futures-util", + "livekit-common", + "livekit-protocol", + "log", + "parking_lot", + "prost 0.12.6", + "thiserror 2.0.18", + "tokio", + "uuid", +] + [[package]] name = "livekit-datatrack" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index c54b1d920..c0db1c077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "livekit", "livekit-api", "livekit-protocol", + "livekit-common", "livekit-ffi", "livekit-uniffi", "livekit-datatrack", @@ -51,6 +52,7 @@ livekit = { version = "0.7.49", path = "livekit" } livekit-api = { version = "0.5.4", path = "livekit-api" } livekit-ffi = { version = "0.12.67", path = "livekit-ffi" } livekit-datatrack = { version = "0.1.9", path = "livekit-datatrack" } +livekit-common = { version = "0.1.0", path = "livekit-common" } livekit-protocol = { version = "0.7.9", path = "livekit-protocol" } livekit-runtime = { version = "0.4.0", path = "livekit-runtime" } soxr-sys = { version = "0.1.3", path = "soxr-sys" } diff --git a/livekit-api/Cargo.toml b/livekit-api/Cargo.toml index b4253e91c..2908e5c37 100644 --- a/livekit-api/Cargo.toml +++ b/livekit-api/Cargo.toml @@ -101,6 +101,7 @@ __rustls-tls = ["tokio-tungstenite?/__rustls-tls", "reqwest?/__rustls"] [dependencies] livekit-protocol = { workspace = true } +livekit-common = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } sha2 = "0.10" diff --git a/livekit-api/src/signal_client/mod.rs b/livekit-api/src/signal_client/mod.rs index 0dd05234a..a9738aca2 100644 --- a/livekit-api/src/signal_client/mod.rs +++ b/livekit-api/src/signal_client/mod.rs @@ -65,14 +65,12 @@ const CLIENT_CAPABILITIES: &[proto::client_info::Capability] = &[ proto::client_info::Capability::CapCompressionDeflateRaw, ]; -/// Default value for `ClientInfo.client_protocol` when a participant has not -/// advertised one (treat as v1-only / no data-stream RPC support). -pub const CLIENT_PROTOCOL_DEFAULT: i32 = 0; -/// `ClientInfo.client_protocol` value indicating support for RPC v2 over data streams. -pub const CLIENT_PROTOCOL_DATA_STREAM_RPC: i32 = 1; -/// `ClientInfo.client_protocol` value indicating support for data streams v2 -/// (inline single-packet sends; compression is gated separately via capabilities). -pub const CLIENT_PROTOCOL_DATA_STREAM_V2: i32 = 2; +// The canonical `client_protocol` constants live in `livekit-common` (shared with the +// data-stream crate); re-exported here so existing `livekit_api::signal_client::CLIENT_PROTOCOL_*` +// references keep resolving. +pub use livekit_common::{ + CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DATA_STREAM_V2, CLIENT_PROTOCOL_DEFAULT, +}; /// The client protocol which is sent to other clients and indicates the set of apis that other /// clients should assume this client supports. diff --git a/livekit/src/proto.rs b/livekit/src/proto.rs index 78e690d73..15d5fdf71 100644 --- a/livekit/src/proto.rs +++ b/livekit/src/proto.rs @@ -141,35 +141,6 @@ impl From for DataPacketKind { } } -impl From for EncryptionType { - fn from(value: livekit_protocol::encryption::Type) -> Self { - match value { - livekit_protocol::encryption::Type::None => Self::None, - livekit_protocol::encryption::Type::Gcm => Self::Gcm, - livekit_protocol::encryption::Type::Custom => Self::Custom, - } - } -} - -impl From for encryption::Type { - fn from(value: EncryptionType) -> Self { - match value { - EncryptionType::None => Self::None, - EncryptionType::Gcm => Self::Gcm, - EncryptionType::Custom => Self::Custom, - } - } -} - -impl From for i32 { - fn from(value: EncryptionType) -> Self { - match value { - EncryptionType::None => 0, - EncryptionType::Gcm => 1, - EncryptionType::Custom => 2, - } - } -} impl From for participant::ParticipantState { fn from(value: participant_info::State) -> Self { diff --git a/livekit/src/room/e2ee/mod.rs b/livekit/src/room/e2ee/mod.rs index e1235d81d..21f6cfca7 100644 --- a/livekit/src/room/e2ee/mod.rs +++ b/livekit/src/room/e2ee/mod.rs @@ -22,13 +22,7 @@ pub mod manager; /// Provider implementations for data track. pub(crate) mod data_track; -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] -pub enum EncryptionType { - #[default] - None, - Gcm, - Custom, -} +pub(crate) use livekit_common::EncryptionType; #[derive(Clone)] pub struct E2eeOptions { diff --git a/livekit/src/room/id.rs b/livekit/src/room/id.rs index 800496371..c1ea11a74 100644 --- a/livekit/src/room/id.rs +++ b/livekit/src/room/id.rs @@ -20,30 +20,17 @@ const ROOM_PREFIX: &str = "RM_"; const PARTICIPANT_PREFIX: &str = "PA_"; const TRACK_PREFIX: &str = "TR_"; +pub use livekit_common::ParticipantIdentity; + #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct ParticipantSid(String); -#[derive(Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -pub struct ParticipantIdentity(pub String); - #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct TrackSid(String); #[derive(Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct RoomSid(String); -impl From for ParticipantIdentity { - fn from(value: String) -> Self { - Self(value) - } -} - -impl From<&str> for ParticipantIdentity { - fn from(value: &str) -> Self { - Self(value.to_string()) - } -} - macro_rules! impl_string_into { ($from:ty) => { impl From<$from> for String { @@ -67,7 +54,6 @@ macro_rules! impl_string_into { } impl_string_into!(ParticipantSid); -impl_string_into!(ParticipantIdentity); impl_string_into!(TrackSid); impl_string_into!(RoomSid); diff --git a/livekit/src/room/mod.rs b/livekit/src/room/mod.rs index f140bcc02..72a0d3205 100644 --- a/livekit/src/room/mod.rs +++ b/livekit/src/room/mod.rs @@ -63,7 +63,7 @@ use crate::{ utils::{observer::Dispatcher, promise::Promise}, }; -pub mod data_stream; +pub use livekit_data_stream as data_stream; pub mod data_track; pub mod e2ee; pub mod id; diff --git a/livekit/src/room/participant/mod.rs b/livekit/src/room/participant/mod.rs index ef049845f..9f8abad0e 100644 --- a/livekit/src/room/participant/mod.rs +++ b/livekit/src/room/participant/mod.rs @@ -85,46 +85,7 @@ pub enum DisconnectReason { AgentError, } -/// A capability a participant's client advertises (mirrors `ClientInfo.Capability`). -/// -/// Stored typed rather than as the raw protobuf `i32` so the public accessor doesn't leak -/// protobuf types, while `Unknown` preserves values this SDK build doesn't recognize. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -#[non_exhaustive] -pub enum ClientCapability { - Unused, - PacketTrailer, - CompressionDeflateRaw, -} - -impl TryFrom for ClientCapability { - type Error = &'static str; - - fn try_from(value: i32) -> Result { - match proto::client_info::Capability::try_from(value) { - Ok(proto::client_info::Capability::CapPacketTrailer) => Ok(Self::PacketTrailer), - Ok(proto::client_info::Capability::CapCompressionDeflateRaw) => { - Ok(Self::CompressionDeflateRaw) - } - Ok(proto::client_info::Capability::CapUnused) => Ok(Self::Unused), - Err(_) => Err("unknown client capability"), - } - } -} - -impl From for i32 { - fn from(value: ClientCapability) -> Self { - match value { - ClientCapability::Unused => proto::client_info::Capability::CapUnused as i32, - ClientCapability::PacketTrailer => { - proto::client_info::Capability::CapPacketTrailer as i32 - } - ClientCapability::CompressionDeflateRaw => { - proto::client_info::Capability::CapCompressionDeflateRaw as i32 - } - } - } -} +pub use livekit_common::ClientCapability; #[derive(Debug, Clone)] pub enum Participant { From 7633348f8eefeb7a03d5ed029fb9bf9424cffda9 Mon Sep 17 00:00:00 2001 From: Ryan Gaus Date: Thu, 2 Jul 2026 17:25:10 -0400 Subject: [PATCH 37/37] fix: remove serde from livekit-common Not needed for now, can be re-added with an feature later --- livekit-common/Cargo.toml | 1 - livekit-common/src/lib.rs | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/livekit-common/Cargo.toml b/livekit-common/Cargo.toml index 0b83ade48..c3ec0491d 100644 --- a/livekit-common/Cargo.toml +++ b/livekit-common/Cargo.toml @@ -9,4 +9,3 @@ repository.workspace = true [dependencies] livekit-protocol = { workspace = true } -serde = { workspace = true, features = ["derive"] } diff --git a/livekit-common/src/lib.rs b/livekit-common/src/lib.rs index 69a11de77..d2c1ac47a 100644 --- a/livekit-common/src/lib.rs +++ b/livekit-common/src/lib.rs @@ -19,7 +19,6 @@ use std::fmt::Display; use livekit_protocol as proto; -use serde::{Deserialize, Serialize}; // ------------------------------------------------------------------------------------------------- // Client protocol @@ -38,7 +37,7 @@ pub const CLIENT_PROTOCOL_DATA_STREAM_V2: i32 = 2; // ParticipantIdentity // ------------------------------------------------------------------------------------------------- -#[derive(Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Clone, Default, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct ParticipantIdentity(pub String); impl From for ParticipantIdentity { @@ -75,7 +74,7 @@ impl ParticipantIdentity { // EncryptionType // ------------------------------------------------------------------------------------------------- -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub enum EncryptionType { #[default] None, @@ -120,7 +119,7 @@ impl From for i32 { /// A capability a participant's client advertises (mirrors `ClientInfo.Capability`). /// /// Stored typed rather than as the raw protobuf `i32` so accessors don't leak protobuf types. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[non_exhaustive] pub enum ClientCapability { Unused,