Skip to main content

gap/
payload.rs

1//! GAP audio payload codec. Five CBOR keys.
2
3use gbp::CodecError;
4use gbp_core::PayloadCodec;
5use serde::{Deserialize, Serialize};
6use serde_bytes::ByteBuf;
7
8/// Audio frame payload.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct GapPayload {
11    /// Audio source identifier (microphone or device).
12    #[serde(rename = "msid")]
13    pub media_source_id: u32,
14    /// 16-bit `rtp_sequence` widened to `u32` for CBOR uint compatibility.
15    #[serde(rename = "seq")]
16    pub rtp_sequence: u32,
17    /// 48 kHz timestamp.
18    #[serde(rename = "ts")]
19    pub rtp_timestamp: u64,
20    /// Key phase (binds the payload to a specific MLS epoch).
21    #[serde(rename = "kp")]
22    pub key_phase: u32,
23    /// Opus frame bytes.
24    #[serde(rename = "opus")]
25    pub opus_frame: ByteBuf,
26}
27
28impl GapPayload {
29    /// Builds a 20 ms Opus frame at 48 kHz (960 samples).
30    pub fn opus_20ms(
31        media_source_id: u32,
32        rtp_sequence: u16,
33        key_phase: u32,
34        opus: Vec<u8>,
35    ) -> Self {
36        Self {
37            media_source_id,
38            rtp_sequence: rtp_sequence as u32,
39            rtp_timestamp: 960,
40            key_phase,
41            opus_frame: ByteBuf::from(opus),
42        }
43    }
44
45    /// Builds an Opus frame with an explicit `rtp_timestamp`.
46    /// Prefer [`GapPayload::opus_20ms`] for the common 48 kHz / 20 ms case.
47    pub fn with_timestamp(
48        media_source_id: u32,
49        rtp_sequence: u16,
50        rtp_timestamp: u64,
51        key_phase: u32,
52        opus: Vec<u8>,
53    ) -> Self {
54        Self {
55            media_source_id,
56            rtp_sequence: rtp_sequence as u32,
57            rtp_timestamp,
58            key_phase,
59            opus_frame: ByteBuf::from(opus),
60        }
61    }
62
63    /// CBOR-encodes the payload.
64    pub fn to_cbor(&self) -> Vec<u8> {
65        let mut buf = Vec::new();
66        ciborium::into_writer(self, &mut buf).expect("cbor encode");
67        buf
68    }
69
70    /// Decodes a CBOR-encoded payload.
71    pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
72        ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
73    }
74
75    /// Encodes using the given codec.
76    pub fn to_bytes(&self, codec: PayloadCodec) -> Vec<u8> {
77        match codec {
78            PayloadCodec::Cbor => self.to_cbor(),
79            PayloadCodec::Protobuf => {
80                use prost::Message as _;
81                gbp_proto::gap::GapPayload::from(self).encode_to_vec()
82            }
83            PayloadCodec::FlatBuffers => {
84                let mut b = gbp_flat::planus::Builder::new();
85                b.finish(gbp_flat::gap::GapPayload::from(self), None).to_vec()
86            }
87        }
88    }
89
90    /// Decodes from the given codec.
91    pub fn from_bytes(data: &[u8], codec: PayloadCodec) -> Result<Self, CodecError> {
92        match codec {
93            PayloadCodec::Cbor => Self::from_cbor(data),
94            PayloadCodec::Protobuf => {
95                use prost::Message as _;
96                let p = gbp_proto::gap::GapPayload::decode(data)
97                    .map_err(|e| CodecError::Decode(e.to_string()))?;
98                Ok(Self::from(p))
99            }
100            PayloadCodec::FlatBuffers => {
101                use gbp_flat::planus::ReadAsRoot as _;
102                let r = gbp_flat::gap::GapPayloadRef::read_as_root(data)
103                    .map_err(|e| CodecError::Decode(e.to_string()))?;
104                Self::try_from(r).map_err(|_| CodecError::Decode("flatbuffers field error".into()))
105            }
106        }
107    }
108}
109
110// ── Proto conversions ─────────────────────────────────────────────────────────
111
112impl From<&GapPayload> for gbp_proto::gap::GapPayload {
113    fn from(p: &GapPayload) -> Self {
114        Self {
115            media_source_id: p.media_source_id,
116            rtp_sequence: p.rtp_sequence,
117            rtp_timestamp: p.rtp_timestamp,
118            key_phase: p.key_phase,
119            opus_frame: p.opus_frame.to_vec(),
120        }
121    }
122}
123
124impl From<gbp_proto::gap::GapPayload> for GapPayload {
125    fn from(p: gbp_proto::gap::GapPayload) -> Self {
126        Self {
127            media_source_id: p.media_source_id,
128            rtp_sequence: p.rtp_sequence,
129            rtp_timestamp: p.rtp_timestamp,
130            key_phase: p.key_phase,
131            opus_frame: ByteBuf::from(p.opus_frame),
132        }
133    }
134}
135
136// ── FlatBuffers conversions ───────────────────────────────────────────────────
137
138impl From<&GapPayload> for gbp_flat::gap::GapPayload {
139    fn from(p: &GapPayload) -> Self {
140        Self {
141            media_source_id: p.media_source_id,
142            rtp_sequence: p.rtp_sequence,
143            rtp_timestamp: p.rtp_timestamp,
144            key_phase: p.key_phase,
145            opus_frame: Some(p.opus_frame.to_vec()),
146        }
147    }
148}
149
150impl<'a> TryFrom<gbp_flat::gap::GapPayloadRef<'a>> for GapPayload {
151    type Error = ();
152    fn try_from(r: gbp_flat::gap::GapPayloadRef<'a>) -> Result<Self, ()> {
153        let opus_frame = r.opus_frame().map_err(|_| ())?.unwrap_or(&[]).to_vec();
154        Ok(Self {
155            media_source_id: r.media_source_id().map_err(|_| ())?,
156            rtp_sequence: r.rtp_sequence().map_err(|_| ())?,
157            rtp_timestamp: r.rtp_timestamp().map_err(|_| ())?,
158            key_phase: r.key_phase().map_err(|_| ())?,
159            opus_frame: ByteBuf::from(opus_frame),
160        })
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn sample() -> GapPayload {
169        GapPayload::opus_20ms(1, 42, 7, vec![0xAB, 0xCD, 0xEF])
170    }
171
172    #[test]
173    fn cbor_roundtrip() {
174        let orig = sample();
175        let bytes = orig.to_bytes(PayloadCodec::Cbor);
176        let decoded = GapPayload::from_bytes(&bytes, PayloadCodec::Cbor).unwrap();
177        assert_eq!(decoded.media_source_id, orig.media_source_id);
178        assert_eq!(decoded.rtp_sequence, orig.rtp_sequence);
179        assert_eq!(decoded.key_phase, orig.key_phase);
180        assert_eq!(decoded.opus_frame.as_ref(), orig.opus_frame.as_ref());
181    }
182
183    #[test]
184    fn protobuf_roundtrip() {
185        let orig = sample();
186        let bytes = orig.to_bytes(PayloadCodec::Protobuf);
187        let decoded = GapPayload::from_bytes(&bytes, PayloadCodec::Protobuf).unwrap();
188        assert_eq!(decoded.media_source_id, orig.media_source_id);
189        assert_eq!(decoded.rtp_sequence, orig.rtp_sequence);
190        assert_eq!(decoded.opus_frame.as_ref(), orig.opus_frame.as_ref());
191    }
192
193    #[test]
194    fn flatbuffers_roundtrip() {
195        let orig = sample();
196        let bytes = orig.to_bytes(PayloadCodec::FlatBuffers);
197        let decoded = GapPayload::from_bytes(&bytes, PayloadCodec::FlatBuffers).unwrap();
198        assert_eq!(decoded.media_source_id, orig.media_source_id);
199        assert_eq!(decoded.rtp_sequence, orig.rtp_sequence);
200        assert_eq!(decoded.opus_frame.as_ref(), orig.opus_frame.as_ref());
201    }
202
203    #[test]
204    fn codec_bytes_differ() {
205        let p = sample();
206        let cbor = p.to_bytes(PayloadCodec::Cbor);
207        let proto = p.to_bytes(PayloadCodec::Protobuf);
208        let flat = p.to_bytes(PayloadCodec::FlatBuffers);
209        assert_ne!(cbor, proto);
210        assert_ne!(cbor, flat);
211        assert_ne!(proto, flat);
212    }
213}