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)
86                    .to_vec()
87            }
88        }
89    }
90
91    /// Decodes from the given codec.
92    pub fn from_bytes(data: &[u8], codec: PayloadCodec) -> Result<Self, CodecError> {
93        match codec {
94            PayloadCodec::Cbor => Self::from_cbor(data),
95            PayloadCodec::Protobuf => {
96                use prost::Message as _;
97                let p = gbp_proto::gap::GapPayload::decode(data)
98                    .map_err(|e| CodecError::Decode(e.to_string()))?;
99                Ok(Self::from(p))
100            }
101            PayloadCodec::FlatBuffers => {
102                use gbp_flat::planus::ReadAsRoot as _;
103                let r = gbp_flat::gap::GapPayloadRef::read_as_root(data)
104                    .map_err(|e| CodecError::Decode(e.to_string()))?;
105                Self::try_from(r).map_err(|_| CodecError::Decode("flatbuffers field error".into()))
106            }
107        }
108    }
109}
110
111// ── Proto conversions ─────────────────────────────────────────────────────────
112
113impl From<&GapPayload> for gbp_proto::gap::GapPayload {
114    fn from(p: &GapPayload) -> Self {
115        Self {
116            media_source_id: p.media_source_id,
117            rtp_sequence: p.rtp_sequence,
118            rtp_timestamp: p.rtp_timestamp,
119            key_phase: p.key_phase,
120            opus_frame: p.opus_frame.to_vec(),
121        }
122    }
123}
124
125impl From<gbp_proto::gap::GapPayload> for GapPayload {
126    fn from(p: gbp_proto::gap::GapPayload) -> Self {
127        Self {
128            media_source_id: p.media_source_id,
129            rtp_sequence: p.rtp_sequence,
130            rtp_timestamp: p.rtp_timestamp,
131            key_phase: p.key_phase,
132            opus_frame: ByteBuf::from(p.opus_frame),
133        }
134    }
135}
136
137// ── FlatBuffers conversions ───────────────────────────────────────────────────
138
139impl From<&GapPayload> for gbp_flat::gap::GapPayload {
140    fn from(p: &GapPayload) -> Self {
141        Self {
142            media_source_id: p.media_source_id,
143            rtp_sequence: p.rtp_sequence,
144            rtp_timestamp: p.rtp_timestamp,
145            key_phase: p.key_phase,
146            opus_frame: Some(p.opus_frame.to_vec()),
147        }
148    }
149}
150
151impl<'a> TryFrom<gbp_flat::gap::GapPayloadRef<'a>> for GapPayload {
152    type Error = ();
153    fn try_from(r: gbp_flat::gap::GapPayloadRef<'a>) -> Result<Self, ()> {
154        let opus_frame = r.opus_frame().map_err(|_| ())?.unwrap_or(&[]).to_vec();
155        Ok(Self {
156            media_source_id: r.media_source_id().map_err(|_| ())?,
157            rtp_sequence: r.rtp_sequence().map_err(|_| ())?,
158            rtp_timestamp: r.rtp_timestamp().map_err(|_| ())?,
159            key_phase: r.key_phase().map_err(|_| ())?,
160            opus_frame: ByteBuf::from(opus_frame),
161        })
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    fn sample() -> GapPayload {
170        GapPayload::opus_20ms(1, 42, 7, vec![0xAB, 0xCD, 0xEF])
171    }
172
173    #[test]
174    fn cbor_roundtrip() {
175        let orig = sample();
176        let bytes = orig.to_bytes(PayloadCodec::Cbor);
177        let decoded = GapPayload::from_bytes(&bytes, PayloadCodec::Cbor).unwrap();
178        assert_eq!(decoded.media_source_id, orig.media_source_id);
179        assert_eq!(decoded.rtp_sequence, orig.rtp_sequence);
180        assert_eq!(decoded.key_phase, orig.key_phase);
181        assert_eq!(decoded.opus_frame.as_ref(), orig.opus_frame.as_ref());
182    }
183
184    #[test]
185    fn protobuf_roundtrip() {
186        let orig = sample();
187        let bytes = orig.to_bytes(PayloadCodec::Protobuf);
188        let decoded = GapPayload::from_bytes(&bytes, PayloadCodec::Protobuf).unwrap();
189        assert_eq!(decoded.media_source_id, orig.media_source_id);
190        assert_eq!(decoded.rtp_sequence, orig.rtp_sequence);
191        assert_eq!(decoded.opus_frame.as_ref(), orig.opus_frame.as_ref());
192    }
193
194    #[test]
195    fn flatbuffers_roundtrip() {
196        let orig = sample();
197        let bytes = orig.to_bytes(PayloadCodec::FlatBuffers);
198        let decoded = GapPayload::from_bytes(&bytes, PayloadCodec::FlatBuffers).unwrap();
199        assert_eq!(decoded.media_source_id, orig.media_source_id);
200        assert_eq!(decoded.rtp_sequence, orig.rtp_sequence);
201        assert_eq!(decoded.opus_frame.as_ref(), orig.opus_frame.as_ref());
202    }
203
204    #[test]
205    fn codec_bytes_differ() {
206        let p = sample();
207        let cbor = p.to_bytes(PayloadCodec::Cbor);
208        let proto = p.to_bytes(PayloadCodec::Protobuf);
209        let flat = p.to_bytes(PayloadCodec::FlatBuffers);
210        assert_ne!(cbor, proto);
211        assert_ne!(cbor, flat);
212        assert_ne!(proto, flat);
213    }
214}