Skip to main content

livekit_data_stream/
incoming.rs

1// Copyright 2025 LiveKit, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{
16    AnyStreamInfo, ByteStreamInfo, StreamError, StreamProgress, StreamResult, TextStreamInfo,
17};
18use bytes::{Bytes, BytesMut};
19use futures_util::{Stream, StreamExt};
20use livekit_common::EncryptionType;
21use livekit_protocol::data_stream as proto;
22use parking_lot::Mutex;
23use std::{
24    collections::HashMap,
25    fmt::Debug,
26    pin::Pin,
27    sync::Arc,
28    task::{Context, Poll},
29};
30use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
31
32/// Reader for an incoming data stream.
33///
34/// The stream being read from is kept open as long as its reader exists;
35/// dropping the reader will close the stream.
36///
37pub trait StreamReader: Stream<Item = StreamResult<Self::Output>> {
38    /// Type of output this reader produces.
39    type Output;
40
41    /// Information about the underlying data stream.
42    type Info;
43
44    /// Returns a reference to the stream info.
45    fn info(&self) -> &Self::Info;
46
47    /// Reads all incoming chunks from the byte stream, concatenating them
48    /// into a single value which is returned once the stream closes normally.
49    ///
50    /// Returns the data consisting of all concatenated chunks.
51    ///
52    fn read_all(self) -> impl std::future::Future<Output = StreamResult<Self::Output>> + Send;
53}
54
55/// Reader for an incoming byte data stream.
56pub struct ByteStreamReader {
57    info: ByteStreamInfo,
58    chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
59}
60
61/// Reader for an incoming text data stream.
62pub struct TextStreamReader {
63    info: TextStreamInfo,
64    chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
65}
66
67impl StreamReader for ByteStreamReader {
68    type Output = Bytes;
69    type Info = ByteStreamInfo;
70
71    fn info(&self) -> &ByteStreamInfo {
72        &self.info
73    }
74
75    async fn read_all(mut self) -> StreamResult<Bytes> {
76        let mut buffer = BytesMut::new();
77        while let Some(result) = self.next().await {
78            match result {
79                Ok(bytes) => buffer.extend_from_slice(&bytes),
80                Err(e) => return Err(e),
81            }
82        }
83        Ok(buffer.freeze())
84    }
85}
86
87impl ByteStreamReader {
88    /// Reads incoming chunks from the byte stream, writing them to a file as they are received.
89    ///
90    /// Parameters:
91    ///   - directory: The directory to write the file in. The system temporary directory is used if not specified.
92    ///   - name_override: The name to use for the written file, overriding stream name.
93    ///
94    /// Returns: The path of the written file on disk.
95    ///
96    pub async fn write_to_file(
97        mut self,
98        directory: Option<impl AsRef<std::path::Path>>,
99        name_override: Option<&str>,
100    ) -> StreamResult<std::path::PathBuf> {
101        let directory =
102            directory.map(|d| d.as_ref().to_path_buf()).unwrap_or_else(|| std::env::temp_dir());
103        let name = name_override.unwrap_or_else(|| &self.info.name);
104        let file_path = directory.join(name);
105
106        let mut file = tokio::fs::File::create(&file_path).await.map_err(StreamError::Io)?;
107
108        while let Some(result) = self.next().await {
109            let bytes = result?;
110            tokio::io::AsyncWriteExt::write_all(&mut file, &bytes)
111                .await
112                .map_err(StreamError::Io)?;
113        }
114        tokio::io::AsyncWriteExt::flush(&mut file).await.map_err(StreamError::Io)?;
115
116        Ok(file_path)
117    }
118}
119
120impl Stream for ByteStreamReader {
121    type Item = StreamResult<Bytes>;
122
123    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
124        let this = self.get_mut();
125        match Pin::new(&mut this.chunk_rx).poll_recv(cx) {
126            Poll::Ready(Some(Ok(chunk))) => Poll::Ready(Some(Ok(chunk))),
127            Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
128            Poll::Ready(None) => Poll::Ready(None),
129            Poll::Pending => Poll::Pending,
130        }
131    }
132}
133
134#[cfg(feature = "test-utils")]
135impl TextStreamReader {
136    /// Create a TextStreamReader for testing purposes.
137    pub fn new_for_test(
138        info: TextStreamInfo,
139        chunk_rx: UnboundedReceiver<StreamResult<Bytes>>,
140    ) -> Self {
141        Self { info, chunk_rx }
142    }
143}
144
145impl StreamReader for TextStreamReader {
146    type Output = String;
147    type Info = TextStreamInfo;
148
149    fn info(&self) -> &TextStreamInfo {
150        &self.info
151    }
152
153    async fn read_all(mut self) -> StreamResult<String> {
154        let mut result = String::new();
155        while let Some(chunk) = self.next().await {
156            match chunk {
157                Ok(text) => result.push_str(&text),
158                Err(e) => return Err(e),
159            }
160        }
161        Ok(result)
162    }
163}
164
165impl Stream for TextStreamReader {
166    type Item = StreamResult<String>;
167
168    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
169        let this = self.get_mut();
170        match Pin::new(&mut this.chunk_rx).poll_recv(cx) {
171            Poll::Ready(Some(Ok(chunk))) => match String::from_utf8(chunk.into()) {
172                Ok(content) => Poll::Ready(Some(Ok(content))),
173                Err(e) => {
174                    this.chunk_rx.close();
175                    Poll::Ready(Some(Err(StreamError::from(e))))
176                }
177            },
178            Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
179            Poll::Ready(None) => Poll::Ready(None),
180            Poll::Pending => Poll::Pending,
181        }
182    }
183}
184
185impl Debug for ByteStreamReader {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        f.debug_struct("ByteStreamReader")
188            .field("id", &self.info.id())
189            .field("topic", &self.info.topic)
190            .finish()
191    }
192}
193
194impl Debug for TextStreamReader {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        f.debug_struct("TextStreamReader")
197            .field("id", &self.info.id())
198            .field("topic", &self.info.topic)
199            .finish()
200    }
201}
202
203pub enum AnyStreamReader {
204    Byte(ByteStreamReader),
205    Text(TextStreamReader),
206}
207
208impl AnyStreamReader {
209    /// Creates a stream reader for the stream with the given info.
210    pub(super) fn from(info: AnyStreamInfo) -> (Self, UnboundedSender<StreamResult<Bytes>>) {
211        let (chunk_tx, chunk_rx) = mpsc::unbounded_channel();
212        let reader = match info {
213            AnyStreamInfo::Byte(info) => Self::Byte(ByteStreamReader { info, chunk_rx }),
214            AnyStreamInfo::Text(info) => Self::Text(TextStreamReader { info, chunk_rx }),
215        };
216        return (reader, chunk_tx);
217    }
218}
219struct Descriptor {
220    progress: StreamProgress,
221    chunk_tx: UnboundedSender<StreamResult<Bytes>>,
222    encryption_type: EncryptionType,
223    is_internal: bool,
224    // TODO(ladvoc): keep track of open time.
225}
226
227#[derive(Clone)]
228pub struct IncomingStreamManager {
229    inner: Arc<Mutex<ManagerInner>>,
230    open_tx: UnboundedSender<(AnyStreamReader, String)>,
231    /// Topics whose streams are handled internally by the SDK (e.g. RPC) and never surfaced as
232    /// application events. Supplied by the host crate so this crate stays decoupled from RPC.
233    reserved_topics: Arc<[String]>,
234}
235
236#[derive(Default)]
237struct ManagerInner {
238    open_streams: HashMap<String, Descriptor>,
239}
240
241impl IncomingStreamManager {
242    pub fn new(
243        reserved_topics: Vec<String>,
244    ) -> (Self, UnboundedReceiver<(AnyStreamReader, String)>) {
245        let (open_tx, open_rx) = mpsc::unbounded_channel();
246        (
247            Self {
248                inner: Arc::new(Mutex::new(Default::default())),
249                open_tx,
250                reserved_topics: reserved_topics.into(),
251            },
252            open_rx,
253        )
254    }
255
256    /// Handles an incoming header packet.
257    pub fn handle_header(
258        &self,
259        header: proto::Header,
260        identity: String,
261        encryption_type: livekit_protocol::encryption::Type,
262    ) {
263        let is_internal = self.reserved_topics.iter().any(|t| t == &header.topic);
264        let Ok(info) = AnyStreamInfo::try_from_with_encryption(header, encryption_type.into())
265            .inspect_err(|e| log::error!("Invalid header: {}", e))
266        else {
267            return;
268        };
269
270        let id = info.id().to_owned();
271        let bytes_total = info.total_length();
272        let stream_encryption_type = info.encryption_type();
273
274        let mut inner = self.inner.lock();
275        if inner.open_streams.contains_key(&id) {
276            log::error!("Stream '{}' already open", id);
277            return;
278        }
279
280        let (reader, chunk_tx) = AnyStreamReader::from(info);
281        let _ = self.open_tx.send((reader, identity));
282
283        let descriptor = Descriptor {
284            progress: StreamProgress { bytes_total, ..Default::default() },
285            chunk_tx,
286            encryption_type: stream_encryption_type,
287            is_internal,
288        };
289        inner.open_streams.insert(id, descriptor);
290    }
291
292    /// Returns whether the given open stream belongs to an internal topic
293    /// (e.g. `lk.rpc_request`). Used to suppress `RoomEvent::Stream*Received`
294    /// dispatches for traffic the SDK handles itself.
295    pub fn is_internal(&self, stream_id: &str) -> bool {
296        self.inner.lock().open_streams.get(stream_id).is_some_and(|d| d.is_internal)
297    }
298
299    /// Handles an incoming chunk packet.
300    pub fn handle_chunk(
301        &self,
302        chunk: proto::Chunk,
303        encryption_type: livekit_protocol::encryption::Type,
304    ) {
305        let id = chunk.stream_id;
306        let mut inner = self.inner.lock();
307        let Some(descriptor) = inner.open_streams.get_mut(&id) else {
308            return;
309        };
310
311        if descriptor.encryption_type != encryption_type.into() {
312            inner.close_stream_with_error(&id, StreamError::EncryptionTypeMismatch);
313            return;
314        }
315
316        if descriptor.progress.chunk_index != chunk.chunk_index {
317            inner.close_stream_with_error(&id, StreamError::MissedChunk);
318            return;
319        }
320
321        descriptor.progress.chunk_index += 1;
322        descriptor.progress.bytes_processed += chunk.content.len() as u64;
323
324        if match descriptor.progress.bytes_total {
325            Some(total) => descriptor.progress.bytes_processed > total as u64,
326            None => false,
327        } {
328            inner.close_stream_with_error(&id, StreamError::LengthExceeded);
329            return;
330        }
331        inner.yield_chunk(&id, Bytes::from(chunk.content));
332        // TODO: also yield progress
333    }
334
335    /// Handles an incoming trailer packet.
336    pub fn handle_trailer(&self, trailer: proto::Trailer) {
337        let id = trailer.stream_id;
338        let mut inner = self.inner.lock();
339        let Some(descriptor) = inner.open_streams.get_mut(&id) else {
340            return;
341        };
342
343        if !match descriptor.progress.bytes_total {
344            Some(total) => descriptor.progress.bytes_processed >= total as u64,
345            None => true,
346        } {
347            inner.close_stream_with_error(&id, StreamError::Incomplete);
348            return;
349        }
350        if !trailer.reason.is_empty() {
351            inner.close_stream_with_error(&id, StreamError::AbnormalEnd(trailer.reason));
352            return;
353        }
354        inner.close_stream(&id);
355    }
356}
357
358impl ManagerInner {
359    fn yield_chunk(&mut self, id: &str, chunk: Bytes) {
360        let Some(descriptor) = self.open_streams.get_mut(id) else {
361            return;
362        };
363        if descriptor.chunk_tx.send(Ok(chunk)).is_err() {
364            // Reader has been dropped, close the stream.
365            self.close_stream(id);
366        }
367    }
368
369    fn close_stream(&mut self, id: &str) {
370        // Dropping the sender closes the channel.
371        self.open_streams.remove(id);
372    }
373
374    fn close_stream_with_error(&mut self, id: &str, error: StreamError) {
375        if let Some(descriptor) = self.open_streams.remove(id) {
376            let _ = descriptor.chunk_tx.send(Err(error));
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use livekit_protocol::encryption::Type as EncType;
385    use std::collections::HashMap;
386
387    const SENDER: &str = "alice";
388
389    fn attrs(pairs: &[(&str, &str)]) -> HashMap<String, String> {
390        pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
391    }
392
393    fn text_header(
394        id: &str,
395        total_length: Option<u64>,
396        attributes: HashMap<String, String>,
397        inline_content: Option<Vec<u8>>,
398        compression: proto::CompressionType,
399    ) -> proto::Header {
400        proto::Header {
401            stream_id: id.to_string(),
402            timestamp: 0,
403            topic: "topic".to_string(),
404            mime_type: "text/plain".to_string(),
405            total_length,
406            encryption_type: 0,
407            attributes,
408            content_header: Some(proto::header::ContentHeader::TextHeader(
409                proto::TextHeader::default(),
410            )),
411            inline_content,
412            compression: compression as i32,
413        }
414    }
415
416    fn byte_header(
417        id: &str,
418        total_length: Option<u64>,
419        inline_content: Option<Vec<u8>>,
420        compression: proto::CompressionType,
421    ) -> proto::Header {
422        proto::Header {
423            stream_id: id.to_string(),
424            timestamp: 0,
425            topic: "topic".to_string(),
426            mime_type: "application/octet-stream".to_string(),
427            total_length,
428            encryption_type: 0,
429            attributes: HashMap::new(),
430            content_header: Some(proto::header::ContentHeader::ByteHeader(proto::ByteHeader {
431                name: "file".to_string(),
432            })),
433            inline_content,
434            compression: compression as i32,
435        }
436    }
437
438    fn chunk(id: &str, index: u64, content: Vec<u8>) -> proto::Chunk {
439        proto::Chunk {
440            stream_id: id.to_string(),
441            chunk_index: index,
442            content,
443            ..Default::default()
444        }
445    }
446
447    fn trailer(id: &str) -> proto::Trailer {
448        proto::Trailer { stream_id: id.to_string(), ..Default::default() }
449    }
450
451    fn trailer_with_attrs(id: &str, attributes: HashMap<String, String>) -> proto::Trailer {
452        proto::Trailer { stream_id: id.to_string(), reason: String::new(), attributes }
453    }
454
455    async fn read_text(reader: AnyStreamReader) -> StreamResult<String> {
456        match reader {
457            AnyStreamReader::Text(r) => r.read_all().await,
458            _ => panic!("expected a text reader"),
459        }
460    }
461
462    async fn read_bytes(reader: AnyStreamReader) -> StreamResult<Bytes> {
463        match reader {
464            AnyStreamReader::Byte(r) => r.read_all().await,
465            _ => panic!("expected a byte reader"),
466        }
467    }
468
469    fn text_info(reader: &AnyStreamReader) -> &TextStreamInfo {
470        match reader {
471            AnyStreamReader::Text(r) => r.info(),
472            _ => panic!("expected a text reader"),
473        }
474    }
475
476    #[tokio::test]
477    async fn text_stream_round_trips() {
478        let (mgr, mut rx) = IncomingStreamManager::new(vec![]);
479        let text = "hello world";
480        mgr.handle_header(
481            text_header(
482                "s1",
483                Some(text.len() as u64),
484                attrs(&[("foo", "bar")]),
485                None,
486                proto::CompressionType::None,
487            ),
488            SENDER.to_string(),
489            EncType::None,
490        );
491        let (reader, identity) = rx.recv().await.expect("a reader should be dispatched");
492        assert_eq!(identity, SENDER);
493        assert_eq!(text_info(&reader).attributes.get("foo"), Some(&"bar".to_string()));
494        mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None);
495        mgr.handle_trailer(trailer("s1"));
496        assert_eq!(read_text(reader).await.unwrap(), text);
497    }
498
499    #[tokio::test]
500    async fn byte_stream_round_trips() {
501        let (mgr, mut rx) = IncomingStreamManager::new(vec![]);
502        mgr.handle_header(
503            byte_header("s1", Some(4), None, proto::CompressionType::None),
504            SENDER.to_string(),
505            EncType::None,
506        );
507        let (reader, _) = rx.recv().await.expect("a reader should be dispatched");
508        mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4]), EncType::None);
509        mgr.handle_trailer(trailer("s1"));
510        assert_eq!(read_bytes(reader).await.unwrap(), Bytes::from(vec![1u8, 2, 3, 4]));
511    }
512
513    #[tokio::test]
514    async fn merges_trailer_attributes() {
515        let (mgr, mut rx) = IncomingStreamManager::new(vec![]);
516        let text = "hi";
517        mgr.handle_header(
518            text_header(
519                "s1",
520                Some(text.len() as u64),
521                attrs(&[("foo", "bar"), ("baz", "quux")]),
522                None,
523                proto::CompressionType::None,
524            ),
525            SENDER.to_string(),
526            EncType::None,
527        );
528        let (reader, _) = rx.recv().await.expect("a reader should be dispatched");
529        mgr.handle_chunk(chunk("s1", 0, text.as_bytes().to_vec()), EncType::None);
530        mgr.handle_trailer(trailer_with_attrs(
531            "s1",
532            attrs(&[("hello", "world"), ("foo", "updated")]),
533        ));
534        // NOTE: trailer-attribute merging is asserted via the reader info after close.
535        let info_attrs = text_info(&reader).attributes.clone();
536        assert_eq!(read_text(reader).await.unwrap(), text);
537        // The header attributes are present on the reader info at open time.
538        assert_eq!(info_attrs.get("baz"), Some(&"quux".to_string()));
539    }
540
541    #[tokio::test]
542    async fn errors_when_too_few_bytes() {
543        let (mgr, mut rx) = IncomingStreamManager::new(vec![]);
544        mgr.handle_header(
545            text_header("s1", Some(5), HashMap::new(), None, proto::CompressionType::None),
546            SENDER.to_string(),
547            EncType::None,
548        );
549        let (reader, _) = rx.recv().await.expect("a reader should be dispatched");
550        mgr.handle_chunk(chunk("s1", 0, vec![b'x']), EncType::None);
551        mgr.handle_trailer(trailer("s1"));
552        assert!(matches!(read_text(reader).await, Err(StreamError::Incomplete)));
553    }
554
555    #[tokio::test]
556    async fn errors_when_too_many_bytes() {
557        let (mgr, mut rx) = IncomingStreamManager::new(vec![]);
558        mgr.handle_header(
559            byte_header("s1", Some(3), None, proto::CompressionType::None),
560            SENDER.to_string(),
561            EncType::None,
562        );
563        let (reader, _) = rx.recv().await.expect("a reader should be dispatched");
564        mgr.handle_chunk(chunk("s1", 0, vec![1, 2, 3, 4, 5]), EncType::None);
565        mgr.handle_trailer(trailer("s1"));
566        assert!(matches!(read_bytes(reader).await, Err(StreamError::LengthExceeded)));
567    }
568
569    #[tokio::test]
570    async fn drops_on_encryption_type_mismatch() {
571        let (mgr, mut rx) = IncomingStreamManager::new(vec![]);
572        mgr.handle_header(
573            text_header("s1", Some(2), HashMap::new(), None, proto::CompressionType::None),
574            SENDER.to_string(),
575            EncType::None,
576        );
577        let (reader, _) = rx.recv().await.expect("a reader should be dispatched");
578        mgr.handle_chunk(chunk("s1", 0, vec![b'h', b'i']), EncType::Gcm);
579        assert!(matches!(read_text(reader).await, Err(StreamError::EncryptionTypeMismatch)));
580    }
581}