Skip to content

Commit 260f7c7

Browse files
Data track schema metadata (#1159)
Adds support for associating schema metadata with a published data track and storing/retrieving schema definitions. Schema storage is built on-top of data blobs, a general purpose mechanism for storing large (in the order of KBs), arbitrary data blobs in a room: - Protocol: livekit/protocol#1595 - SFU implementation: https://github.com/livekit/livekit/tree/raja_async_attributes Protocol additions for schema metadata: - livekit/protocol#1553 Closes BOT-368 --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 12a112e commit 260f7c7

29 files changed

Lines changed: 3621 additions & 97 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
livekit-api: patch
3+
livekit-datatrack: minor
4+
livekit-ffi: patch
5+
livekit-protocol: patch
6+
livekit-uniffi: patch
7+
livekit: patch
8+
---
9+
10+
Data tracks schema metadata support.

.github/workflows/tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,9 @@ jobs:
151151
uses: livekit/dev-server-action@61e2b4dcb170dd3591e0c9b0db3c3fe5db93b500
152152
with:
153153
github-token: ${{ github.token }}
154-
154+
config: |
155+
# TODO: Remove once this is enabled by default.
156+
enable_participant_data_blob: true
155157
- name: Test
156158
env:
157159
RUST_LOG: info

livekit-datatrack/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
/// Common types for local and remote tracks.
1818
mod track;
1919

20+
/// Schema and frame encoding metadata for typed tracks.
21+
mod schema;
22+
2023
/// Local track publication.
2124
mod local;
2225

@@ -40,7 +43,7 @@ mod error;
4043

4144
/// Public APIs re-exported by client SDKs.
4245
pub mod api {
43-
pub use crate::{error::*, frame::*, local::*, remote::*, track::*};
46+
pub use crate::{error::*, frame::*, local::*, remote::*, schema::*, track::*};
4447
}
4548

4649
/// Internal APIs used within client SDKs to power data tracks functionality.

livekit-datatrack/src/local/events.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use crate::{
1616
api::{DataTrackInfo, DataTrackOptions, LocalDataTrack, PublishError},
1717
packet::Handle,
18+
schema::{DataTrackFrameEncoding, DataTrackSchemaId},
1819
};
1920
use bytes::Bytes;
2021
use from_variants::FromVariants;
@@ -124,6 +125,8 @@ pub struct SfuPublishRequest {
124125
pub handle: Handle,
125126
pub name: String,
126127
pub uses_e2ee: bool,
128+
pub schema: Option<DataTrackSchemaId>,
129+
pub frame_encoding: Option<DataTrackFrameEncoding>,
127130
}
128131

129132
/// Request sent to the SFU to unpublish a track.

livekit-datatrack/src/local/manager.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ impl Manager {
108108
}
109109

110110
async fn on_publish_request(&mut self, event: PublishRequest) {
111+
if let Err(error) = crate::schema::validate_schema(
112+
event.options.frame_encoding.as_ref(),
113+
event.options.schema.as_ref().map(|id| id.encoding()),
114+
) {
115+
_ = event.result_tx.send(Err(PublishError::InvalidSchema(error)));
116+
return;
117+
}
118+
111119
let Some(handle) = self.handle_allocator.get() else {
112120
_ = event.result_tx.send(Err(PublishError::LimitReached));
113121
return;
@@ -134,6 +142,8 @@ impl Manager {
134142
handle,
135143
name: event.options.name,
136144
uses_e2ee: self.encryption_provider.is_some(),
145+
schema: event.options.schema,
146+
frame_encoding: event.options.frame_encoding,
137147
};
138148
_ = self.event_out_tx.send(event.into()).await;
139149
}
@@ -286,6 +296,8 @@ impl Manager {
286296
handle: info.pub_handle,
287297
name: info.name.clone(),
288298
uses_e2ee: info.uses_e2ee,
299+
schema: info.schema.clone(),
300+
frame_encoding: info.frame_encoding.clone(),
289301
};
290302
_ = state_tx.send(PublishState::Republishing);
291303
_ = self.event_out_tx.send(event.into()).await;
@@ -543,6 +555,8 @@ mod tests {
543555
pub_handle,
544556
name: event.name,
545557
uses_e2ee: event.uses_e2ee,
558+
schema: None,
559+
frame_encoding: None,
546560
};
547561
let event = SfuPublishResponse { handle: event.handle, result: Ok(info) };
548562
_ = input.send(event.into());
@@ -622,6 +636,8 @@ mod tests {
622636
pub_handle: handle,
623637
name: "test".into(),
624638
uses_e2ee: false,
639+
schema: None,
640+
frame_encoding: None,
625641
};
626642
let event = SfuPublishResponse { handle, result: Ok(info) };
627643
input.send(event.into()).unwrap();
@@ -652,6 +668,8 @@ mod tests {
652668
pub_handle: event.handle,
653669
name: "secure".into(),
654670
uses_e2ee: true,
671+
schema: None,
672+
frame_encoding: None,
655673
};
656674
let event = SfuPublishResponse { handle: event.handle, result: Ok(info) };
657675
input.send(event.into()).unwrap();
@@ -692,6 +710,8 @@ mod tests {
692710
pub_handle: handle,
693711
name: track_name.clone(),
694712
uses_e2ee: false,
713+
schema: None,
714+
frame_encoding: None,
695715
};
696716
let event = SfuPublishResponse { handle, result: Ok(info) };
697717
input.send(event.into()).unwrap();
@@ -717,6 +737,8 @@ mod tests {
717737
pub_handle: handle,
718738
name: track_name.clone(),
719739
uses_e2ee: false,
740+
schema: None,
741+
frame_encoding: None,
720742
};
721743
let event = SfuPublishResponse { handle, result: Ok(info) };
722744
input.send(event.into()).unwrap();
@@ -746,6 +768,8 @@ mod tests {
746768
pub_handle: event.handle,
747769
name: name.into(),
748770
uses_e2ee: false,
771+
schema: None,
772+
frame_encoding: None,
749773
};
750774
let event = SfuPublishResponse { handle: event.handle, result: Ok(info) };
751775
input.send(event.into()).unwrap();
@@ -785,6 +809,8 @@ mod tests {
785809
pub_handle: event.handle,
786810
name: "active".into(),
787811
uses_e2ee: false,
812+
schema: None,
813+
frame_encoding: None,
788814
};
789815
let event = SfuPublishResponse { handle: event.handle, result: Ok(info) };
790816
input.send(event.into()).unwrap();

livekit-datatrack/src/local/mod.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
// limitations under the License.
1414

1515
use crate::{
16-
api::{DataTrack, DataTrackFrame, DataTrackInfo, InternalError},
16+
api::{DataTrack, DataTrackFrame, DataTrackInfo, DataTrackSchemaError, InternalError},
17+
schema::{DataTrackFrameEncoding, DataTrackSchemaId},
1718
track::DataTrackInner,
1819
};
1920
use std::{fmt, marker::PhantomData, sync::Arc};
@@ -153,6 +154,8 @@ impl Drop for LocalTrackInner {
153154
#[derive(Clone, Debug)]
154155
pub struct DataTrackOptions {
155156
pub(crate) name: String,
157+
pub(crate) schema: Option<DataTrackSchemaId>,
158+
pub(crate) frame_encoding: Option<DataTrackFrameEncoding>,
156159
}
157160

158161
impl DataTrackOptions {
@@ -165,7 +168,17 @@ impl DataTrackOptions {
165168
/// - Must be unique per publisher
166169
///
167170
pub fn new(name: impl Into<String>) -> Self {
168-
Self { name: name.into() }
171+
Self { name: name.into(), schema: None, frame_encoding: None }
172+
}
173+
174+
/// Sets the schema associated with frames sent on the track.
175+
pub fn with_schema(self, schema: DataTrackSchemaId) -> Self {
176+
Self { schema: Some(schema), ..self }
177+
}
178+
179+
/// Sets the encoding of frames sent on the track.
180+
pub fn with_frame_encoding(self, encoding: DataTrackFrameEncoding) -> Self {
181+
Self { frame_encoding: Some(encoding), ..self }
169182
}
170183
}
171184

@@ -216,6 +229,10 @@ pub enum PublishError {
216229
#[error("Room disconnected")]
217230
Disconnected,
218231

232+
/// Schema metadata is invalid.
233+
#[error(transparent)]
234+
InvalidSchema(DataTrackSchemaError),
235+
219236
/// Internal error, please report on GitHub.
220237
#[error(transparent)]
221238
Internal(#[from] InternalError),

livekit-datatrack/src/local/proto.rs

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,15 @@ impl From<SfuPublishRequest> for proto::PublishDataTrackRequest {
3333
fn from(event: SfuPublishRequest) -> Self {
3434
use proto::encryption::Type;
3535
let encryption = if event.uses_e2ee { Type::Gcm } else { Type::None }.into();
36-
Self { pub_handle: event.handle.into(), name: event.name, encryption }
36+
let schema = event.schema.map(Into::into);
37+
let frame_encoding = event.frame_encoding.map(Into::into);
38+
Self {
39+
pub_handle: event.handle.into(),
40+
name: event.name,
41+
encryption,
42+
schema,
43+
frame_encoding,
44+
}
3745
}
3846
}
3947

@@ -74,8 +82,18 @@ impl TryFrom<proto::DataTrackInfo> for DataTrackInfo {
7482
proto::encryption::Type::Gcm => true,
7583
other => Err(anyhow!("Unsupported E2EE type: {:?}", other))?,
7684
};
85+
let frame_encoding = msg.frame_encoding.map(Into::into);
7786
let sid: DataTrackSid = msg.sid.try_into().map_err(anyhow::Error::from)?;
78-
Ok(Self { pub_handle: handle, sid: RwLock::new(sid).into(), name: msg.name, uses_e2ee })
87+
let schema = msg.schema.map(|schema| schema.into());
88+
89+
Ok(Self {
90+
pub_handle: handle,
91+
sid: RwLock::new(sid).into(),
92+
name: msg.name,
93+
uses_e2ee,
94+
schema,
95+
frame_encoding,
96+
})
7997
}
8098
}
8199

@@ -106,12 +124,17 @@ impl From<DataTrackInfo> for proto::DataTrackInfo {
106124
proto::encryption::Type::Gcm
107125
} else {
108126
proto::encryption::Type::None
109-
};
127+
} as i32;
128+
let sid = info.sid().to_string();
129+
let schema = info.schema.map(|schema| schema.into());
130+
let frame_encoding = info.frame_encoding.map(Into::into);
110131
Self {
111132
pub_handle: info.pub_handle.into(),
112-
sid: info.sid().to_string(),
133+
sid,
113134
name: info.name,
114-
encryption: encryption as i32,
135+
encryption,
136+
schema,
137+
frame_encoding,
115138
}
116139
}
117140
}
@@ -128,6 +151,8 @@ pub fn publish_responses_for_sync_state(
128151

129152
#[cfg(test)]
130153
mod tests {
154+
use crate::schema::{DataTrackFrameEncoding, DataTrackSchemaEncoding, DataTrackSchemaId};
155+
131156
use super::*;
132157
use fake::{Fake, Faker};
133158

@@ -137,6 +162,8 @@ mod tests {
137162
handle: 1u32.try_into().unwrap(),
138163
name: "track".into(),
139164
uses_e2ee: true,
165+
schema: None,
166+
frame_encoding: None,
140167
};
141168
let request: proto::PublishDataTrackRequest = event.into();
142169
assert_eq!(request.pub_handle, 1);
@@ -159,6 +186,12 @@ mod tests {
159186
sid: "DTR_1234".into(),
160187
name: "track".into(),
161188
encryption: proto::encryption::Type::Gcm.into(),
189+
schema: proto::DataTrackSchemaId {
190+
name: "schema".into(),
191+
encoding: Some(DataTrackSchemaEncoding::JsonSchema.into()),
192+
}
193+
.into(),
194+
frame_encoding: Some(DataTrackFrameEncoding::Json.into()),
162195
}
163196
.into(),
164197
};
@@ -169,9 +202,43 @@ mod tests {
169202
assert_eq!(info.pub_handle, 1u32.try_into().unwrap());
170203
assert_eq!(*info.sid.read().unwrap(), "DTR_1234".to_string().try_into().unwrap());
171204
assert_eq!(info.name, "track");
205+
assert_eq!(
206+
info.schema,
207+
Some(DataTrackSchemaId::new("schema", DataTrackSchemaEncoding::JsonSchema))
208+
);
209+
assert_eq!(info.frame_encoding, Some(DataTrackFrameEncoding::Json));
172210
assert!(info.uses_e2ee);
173211
}
174212

213+
#[test]
214+
fn test_frame_encoding_mapping() {
215+
let base = proto::DataTrackInfo {
216+
pub_handle: 1,
217+
sid: "DTR_1234".into(),
218+
name: "track".into(),
219+
encryption: proto::encryption::Type::None.into(),
220+
schema: None,
221+
frame_encoding: None,
222+
};
223+
224+
let info: DataTrackInfo = base.clone().try_into().unwrap();
225+
assert_eq!(info.frame_encoding, None);
226+
227+
let unspecified = proto::DataTrackInfo {
228+
frame_encoding: Some(DataTrackFrameEncoding::Other.into()),
229+
..base.clone()
230+
};
231+
let info: DataTrackInfo = unspecified.try_into().unwrap();
232+
assert_eq!(info.frame_encoding, Some(DataTrackFrameEncoding::Other));
233+
234+
let custom = proto::DataTrackInfo {
235+
frame_encoding: Some(DataTrackFrameEncoding::Custom("my_encoding".into()).into()),
236+
..base
237+
};
238+
let info: DataTrackInfo = custom.try_into().unwrap();
239+
assert_eq!(info.frame_encoding, Some(DataTrackFrameEncoding::Custom("my_encoding".into())));
240+
}
241+
175242
#[test]
176243
fn test_from_request_response() {
177244
use proto::request_response::{Reason, Request};

0 commit comments

Comments
 (0)