Skip to main content

o_sfu_protocol/
bundle_api.rs

1//! rust payloads for odoo browser bundle state and updates
2//!
3//! callable compatibility methods stay on `SfuClient`
4//! host projection emits these DTOs through the public [`crate::bundle`] facade
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10pub use crate::core::ConnectionState as BundleConnectionState;
11use crate::{
12    shared::{JsonPayload, RecordingState, RecordingStateUpdate, StopCode, UserId, UserInfo},
13    signaling::TrackBinding,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct BundleStateChange {
18    pub state: BundleConnectionState,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub cause: Option<String>,
21}
22
23/// `info_change` payloads use string keys from [`bundle_session_info_key`]
24/// integer and string user ids that stringify to the same key collide, so the
25/// later entry wins
26pub type BundleSessionInfoSnapshotById = BTreeMap<String, UserInfo>;
27
28#[must_use]
29pub fn bundle_session_info_key(user_id: &UserId) -> String {
30    user_id.path_segment().into_owned()
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct BundleBroadcastUpdate {
35    #[serde(rename = "senderId")]
36    pub sender_id: UserId,
37    pub message: JsonPayload,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct BundleDisconnectUpdate {
42    #[serde(rename = "sessionId")]
43    pub user_id: UserId,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct BundleRemoteMediaUpdate {
48    pub bindings: Vec<TrackBinding>,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct BundleRecordingFlags {
53    pub audio: bool,
54    pub video: bool,
55    pub transcription: bool,
56}
57
58impl From<&RecordingState> for BundleRecordingFlags {
59    fn from(state: &RecordingState) -> Self {
60        // Browser updates require full snapshots. Missing legacy flags default to false.
61        Self {
62            audio: state.audio.unwrap_or(false),
63            video: state.video.unwrap_or(false),
64            transcription: state.transcription.unwrap_or(false),
65        }
66    }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct BundleRecordingStateUpdate {
71    pub state: BundleRecordingFlags,
72    #[serde(rename = "stopCode", skip_serializing_if = "Option::is_none")]
73    pub stop_code: Option<StopCode>,
74}
75
76impl From<&RecordingStateUpdate> for BundleRecordingStateUpdate {
77    fn from(update: &RecordingStateUpdate) -> Self {
78        Self {
79            state: (&update.state).into(),
80            stop_code: update.stop_code,
81        }
82    }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(tag = "name", content = "payload")]
87pub enum BundleUpdate {
88    #[serde(rename = "remote_media")]
89    RemoteMedia(BundleRemoteMediaUpdate),
90    #[serde(rename = "broadcast")]
91    Broadcast(BundleBroadcastUpdate),
92    #[serde(rename = "disconnect")]
93    Disconnect(BundleDisconnectUpdate),
94    #[serde(rename = "info_change")]
95    SessionInfoChange(BundleSessionInfoSnapshotById),
96    #[serde(rename = "channel_info_change")]
97    ChannelInfoChange(BundleRecordingStateUpdate),
98}
99
100#[cfg(test)]
101#[path = "bundle_api/TESTS/mod.rs"]
102mod tests;