Skip to main content

livekit_data_stream/
info.rs

1// Copyright 2026 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 chrono::{DateTime, Utc};
16use livekit_common::EncryptionType;
17use parking_lot::RwLock;
18use std::{collections::HashMap, sync::Arc};
19
20use crate::{
21    types::{ByteHeader, ContentHeader, Header, OperationType, TextHeader},
22    utils::StreamError,
23};
24
25/// Information about a byte data stream.
26#[derive(Clone, Debug)]
27pub struct ByteStreamInfo {
28    /// Unique identifier of the stream.
29    pub id: String,
30    /// Topic name used to route the stream to the appropriate handler.
31    pub topic: String,
32    /// When the stream was created.
33    pub timestamp: DateTime<Utc>,
34    /// Total expected size in bytes, if known.
35    pub total_length: Option<u64>,
36    /// The MIME type of the stream data.
37    pub mime_type: String,
38    /// The name of the file being sent.
39    pub name: String,
40    /// The encryption used
41    pub encryption_type: EncryptionType,
42    /// Test-only: expose whether the byte stream was compressed or not.
43    #[cfg(feature = "test-utils")]
44    pub is_compressed: bool,
45    /// Test-only: expose whether the byte stream was sent inline on the header packet
46    #[cfg(feature = "test-utils")]
47    pub is_inline: bool,
48
49    /// Internal map of attributes which can be updated when new values are received in the trailer
50    /// packet.
51    attributes_map: Arc<RwLock<HashMap<String, String>>>,
52}
53
54impl ByteStreamInfo {
55    /// Additional attributes as needed for your application.
56    ///
57    /// Returns an owned snapshot of the attributes as of this call; attributes may still be
58    /// updated when the trailer packet arrives, so re-call to observe later values.
59    pub fn attributes(&self) -> HashMap<String, String> {
60        self.attributes_map.read().clone()
61    }
62}
63
64/// Information about a text data stream.
65#[derive(Clone, Debug)]
66pub struct TextStreamInfo {
67    /// Unique identifier of the stream.
68    pub id: String,
69    /// Topic name used to route the stream to the appropriate handler.
70    pub topic: String,
71    /// When the stream was created.
72    pub timestamp: DateTime<Utc>,
73    /// Total expected size in bytes, if known.
74    pub total_length: Option<u64>,
75    /// The MIME type of the stream data.
76    pub mime_type: String,
77    pub operation_type: OperationType,
78    pub version: i32,
79    pub reply_to_stream_id: Option<String>,
80    pub attached_stream_ids: Vec<String>,
81    pub generated: bool,
82    /// The encryption used
83    pub encryption_type: EncryptionType,
84    /// Test-only: expose whether the byte stream was compressed or not.
85    #[cfg(feature = "test-utils")]
86    pub is_compressed: bool,
87    /// Test-only: expose whether the byte stream was sent inline on the header packet
88    #[cfg(feature = "test-utils")]
89    pub is_inline: bool,
90
91    /// Internal map of attributes, which can be updated when new values are received in the
92    /// trailer packet.
93    #[doc(hidden)]
94    pub attributes_map: Arc<RwLock<HashMap<String, String>>>,
95}
96
97impl TextStreamInfo {
98    /// Additional attributes as needed for your application.
99    ///
100    /// Returns an owned snapshot of the attributes as of this call; attributes may still be
101    /// updated when the trailer packet arrives, so re-call to observe later values.
102    pub fn attributes(&self) -> HashMap<String, String> {
103        self.attributes_map.read().clone()
104    }
105}
106
107// MARK: - Type conversion
108
109impl TryFrom<Header> for AnyStreamInfo {
110    type Error = StreamError;
111
112    fn try_from(header: Header) -> Result<Self, Self::Error> {
113        Self::try_from_with_encryption(header, EncryptionType::None)
114    }
115}
116
117impl AnyStreamInfo {
118    pub fn try_from_with_encryption(
119        mut header: Header,
120        encryption_type: EncryptionType,
121    ) -> Result<Self, StreamError> {
122        let Some(content_header) = header.content_header.take() else {
123            Err(StreamError::InvalidHeader)?
124        };
125        let info = match content_header {
126            ContentHeader::ByteHeader(byte_header) => Self::Byte(
127                ByteStreamInfo::from_headers_with_encryption(header, byte_header, encryption_type),
128            ),
129            ContentHeader::TextHeader(text_header) => Self::Text(
130                TextStreamInfo::from_headers_with_encryption(header, text_header, encryption_type),
131            ),
132        };
133        Ok(info)
134    }
135}
136
137impl ByteStreamInfo {
138    pub(crate) fn from_headers(header: Header, byte_header: ByteHeader) -> Self {
139        Self::from_headers_with_encryption(header, byte_header, EncryptionType::None)
140    }
141
142    pub(crate) fn from_headers_with_encryption(
143        header: Header,
144        byte_header: ByteHeader,
145        encryption_type: EncryptionType,
146    ) -> Self {
147        Self {
148            id: header.stream_id.to_string(),
149            topic: header.topic,
150            timestamp: DateTime::<Utc>::from_timestamp_millis(header.timestamp)
151                .unwrap_or_else(|| Utc::now()),
152            total_length: header.total_length,
153            attributes_map: Arc::new(RwLock::new(header.attributes)),
154            mime_type: header.mime_type,
155            name: byte_header.name,
156            encryption_type,
157            #[cfg(feature = "test-utils")]
158            is_compressed: header.compression != crate::types::CompressionType::None,
159            #[cfg(feature = "test-utils")]
160            is_inline: header.inline_content.is_some_and(|c| !c.is_empty()),
161        }
162    }
163}
164
165impl TextStreamInfo {
166    pub(crate) fn from_headers(header: Header, text_header: TextHeader) -> Self {
167        Self::from_headers_with_encryption(header, text_header, EncryptionType::None)
168    }
169
170    pub(crate) fn from_headers_with_encryption(
171        header: Header,
172        text_header: TextHeader,
173        encryption_type: EncryptionType,
174    ) -> Self {
175        Self {
176            id: header.stream_id.to_string(),
177            topic: header.topic,
178            timestamp: DateTime::<Utc>::from_timestamp_millis(header.timestamp)
179                .unwrap_or_else(|| Utc::now()),
180            total_length: header.total_length,
181            attributes_map: Arc::new(RwLock::new(header.attributes)),
182            mime_type: header.mime_type,
183            operation_type: text_header.operation_type,
184            version: text_header.version,
185            reply_to_stream_id: text_header.reply_to_stream_id.map(|stream_id| stream_id.into()),
186            attached_stream_ids: text_header
187                .attached_stream_ids
188                .into_iter()
189                .map(Into::into)
190                .collect(),
191            generated: text_header.generated,
192            encryption_type,
193            #[cfg(feature = "test-utils")]
194            is_compressed: header.compression != crate::types::CompressionType::None,
195            #[cfg(feature = "test-utils")]
196            is_inline: header.inline_content.is_some_and(|c| !c.is_empty()),
197        }
198    }
199}
200
201// MARK: - Dispatch
202
203#[derive(Clone, Debug)]
204pub(crate) enum AnyStreamInfo {
205    Byte(ByteStreamInfo),
206    Text(TextStreamInfo),
207}
208
209impl AnyStreamInfo {
210    livekit_common::enum_dispatch!(
211        [Byte, Text];
212        pub fn id(self: &Self) -> &str;
213        pub fn total_length(self: &Self) -> Option<u64>;
214        pub fn encryption_type(self: &Self) -> EncryptionType;
215        pub(crate) fn attributes_map(self: &Self) -> Arc<RwLock<HashMap<String, String>>>;
216    );
217}
218
219#[rustfmt::skip]
220macro_rules! stream_info {
221    () => {
222        pub(crate) fn id(&self) -> &str { &self.id }
223        pub(crate) fn total_length(&self) -> Option<u64> { self.total_length }
224        pub(crate) fn encryption_type(&self) -> EncryptionType { self.encryption_type }
225        pub(crate) fn attributes_map(self: &Self) -> Arc<RwLock<HashMap<String, String>>> {
226            self.attributes_map.clone()
227        }
228    };
229}
230
231impl ByteStreamInfo {
232    stream_info!();
233}
234
235impl TextStreamInfo {
236    stream_info!();
237}
238
239impl From<ByteStreamInfo> for AnyStreamInfo {
240    fn from(info: ByteStreamInfo) -> Self {
241        Self::Byte(info)
242    }
243}
244
245impl From<TextStreamInfo> for AnyStreamInfo {
246    fn from(info: TextStreamInfo) -> Self {
247        Self::Text(info)
248    }
249}