rvoip_core_traits/stream.rs
1use crate::capability::CodecInfo;
2use crate::connection::Direction;
3use crate::data::DataMessage;
4use crate::error::Result;
5use crate::ids::{ConnectionId, StreamId};
6use bytes::Bytes;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::sync::Arc;
11use tokio::sync::mpsc;
12
13/// Provisional ownership of a stream's single-consumer inbound receiver.
14///
15/// Dropping an uncommitted reservation restores the receiver to its stream.
16/// This lets an orchestrator reserve every source needed by a multi-leg media
17/// operation before transferring any one source into a graph. Committing is
18/// the explicit point at which rollback is disabled and the caller becomes
19/// the receiver's permanent owner.
20#[must_use = "dropping an uncommitted media receiver reservation restores it"]
21pub struct MediaReceiverReservation {
22 receiver: Option<mpsc::Receiver<MediaFrame>>,
23 restore: Option<Box<dyn FnOnce(mpsc::Receiver<MediaFrame>) + Send + 'static>>,
24 on_commit: Option<Box<dyn FnOnce() + Send + 'static>>,
25}
26
27impl MediaReceiverReservation {
28 /// Create a rollback-capable receiver reservation.
29 pub fn new(
30 receiver: mpsc::Receiver<MediaFrame>,
31 restore: impl FnOnce(mpsc::Receiver<MediaFrame>) + Send + 'static,
32 ) -> Self {
33 Self {
34 receiver: Some(receiver),
35 restore: Some(Box::new(restore)),
36 on_commit: None,
37 }
38 }
39
40 /// Install a hook that runs only when provisional ownership is committed.
41 ///
42 /// This is primarily useful for ownership accounting. A rolled-back
43 /// reservation must not be counted as a destructive receiver acquisition.
44 pub fn with_commit_hook(mut self, on_commit: impl FnOnce() + Send + 'static) -> Self {
45 self.on_commit = Some(Box::new(on_commit));
46 self
47 }
48
49 /// Transfer permanent ownership of the reserved receiver to the caller.
50 pub fn commit(mut self) -> mpsc::Receiver<MediaFrame> {
51 self.restore.take();
52 if let Some(on_commit) = self.on_commit.take() {
53 on_commit();
54 }
55 self.receiver
56 .take()
57 .expect("media receiver reservation commits exactly once")
58 }
59}
60
61impl Drop for MediaReceiverReservation {
62 fn drop(&mut self) {
63 if let (Some(receiver), Some(restore)) = (self.receiver.take(), self.restore.take()) {
64 restore(receiver);
65 }
66 }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
70pub enum StreamKind {
71 Audio,
72 Video,
73 Data,
74}
75
76#[derive(Clone)]
77pub struct MediaFrame {
78 pub stream_id: StreamId,
79 pub kind: StreamKind,
80 /// Encoded codec payload only.
81 ///
82 /// This field never contains a serialized RTP packet. Adapters remove the
83 /// RTP header before constructing a `MediaFrame` and reconstruct it only
84 /// at their wire boundary. RTP metadata that must survive a bridge lives
85 /// in `timestamp_rtp` and `payload_type`; consumers must not inspect these
86 /// bytes to guess whether an RTP header is present.
87 pub payload: Bytes,
88 pub timestamp_rtp: u32,
89 pub captured_at: DateTime<Utc>,
90 /// RTP payload type for this frame, when known. Set by adapter
91 /// inbound pumps that read the wire RTP header (SIP, WebRTC, QUIC,
92 /// WT). Used by the cross-transport `crate::bridge::frame_pump`
93 /// to route RFC 4733 telephone-events (typically PT 101) to the
94 /// DTMF event sink instead of through the codec transcoder. `None`
95 /// for construction sites that don't have a wire RTP header
96 /// (synthetic test frames, transcoder outputs).
97 ///
98 /// Gap plan §4.3 / CONVERSATION_PROTOCOL.md §7.5.
99 pub payload_type: Option<u8>,
100}
101
102impl fmt::Debug for MediaFrame {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.debug_struct("MediaFrame")
105 .field("stream_id", &self.stream_id)
106 .field("kind", &self.kind)
107 .field("payload_bytes", &self.payload.len())
108 .field("timestamp_rtp", &self.timestamp_rtp)
109 .field("captured_at", &self.captured_at)
110 .field("payload_type", &self.payload_type)
111 .finish()
112 }
113}
114
115#[derive(Clone, Debug, Default)]
116pub struct QualitySnapshot {
117 pub jitter_ms: f32,
118 pub packet_loss_pct: f32,
119 pub mos: Option<f32>,
120}
121
122/// Synchronous decision made for one application data message crossing a
123/// two-connection bridge.
124///
125/// The policy receives the exact source and target connection identities. A
126/// forwarded message may be returned unchanged or replaced with a sanitized
127/// transport-neutral message. Message bodies are deliberately absent from
128/// this type's diagnostics.
129pub enum BridgedDataMessageDecision {
130 Forward(DataMessage),
131 Drop,
132}
133
134impl fmt::Debug for BridgedDataMessageDecision {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 match self {
137 Self::Forward(message) => formatter.debug_tuple("Forward").field(message).finish(),
138 Self::Drop => formatter.write_str("Drop"),
139 }
140 }
141}
142
143/// Non-blocking application policy for bridged [`DataMessage`] traffic.
144///
145/// Implementations must return promptly and must not perform I/O. The
146/// orchestrator evaluates policies on a bridge-owned bounded worker, outside
147/// adapter-event ingest, and validates any transformed message before sending
148/// it to the exact peer connection.
149pub trait DataMessageBridgePolicy: Send + Sync {
150 fn decide(
151 &self,
152 source: &ConnectionId,
153 target: &ConnectionId,
154 message: DataMessage,
155 ) -> BridgedDataMessageDecision;
156}
157
158/// Compatibility policy used by rvoip-core bridge callers that do not install
159/// an application policy.
160#[derive(Clone, Copy, Debug, Default)]
161pub struct PassThroughDataMessageBridgePolicy;
162
163impl DataMessageBridgePolicy for PassThroughDataMessageBridgePolicy {
164 fn decide(
165 &self,
166 _source: &ConnectionId,
167 _target: &ConnectionId,
168 message: DataMessage,
169 ) -> BridgedDataMessageDecision {
170 BridgedDataMessageDecision::Forward(message)
171 }
172}
173
174/// Transport-agnostic media flow. Channel-based per INTERFACE_DESIGN §3.6 to
175/// avoid per-frame async overhead at high frame rates.
176#[async_trait::async_trait]
177pub trait MediaStream: Send + Sync {
178 fn id(&self) -> StreamId;
179 fn kind(&self) -> StreamKind;
180 fn codec(&self) -> CodecInfo;
181 fn direction(&self) -> Direction;
182
183 /// Report whether this stream's source codec and inbound-frame producer are
184 /// ready to be attached to a long-lived consumer.
185 ///
186 /// Most transports publish only fully negotiated streams, so the
187 /// compatibility default is `true`. Deferred transports may expose a stable
188 /// stream identity before SDP/media negotiation finishes; those transports
189 /// must return `false` until [`Self::codec`] is authoritative and a receiver
190 /// acquired through [`Self::reserve_frames_in`] will be driven by the exact
191 /// live route. This probe is deliberately non-destructive.
192 fn source_ready(&self) -> bool {
193 true
194 }
195
196 /// Acquire the stream's inbound receiver.
197 ///
198 /// This legacy API is intentionally retained for source compatibility.
199 /// Built-in streams are single-consumer and return a closed receiver when
200 /// it has already been acquired. New orchestration code should use
201 /// [`Self::try_frames_in`] so duplicate acquisition is reported rather
202 /// than silently behaving like end-of-stream.
203 fn frames_in(&self) -> mpsc::Receiver<MediaFrame>;
204
205 /// Fallibly acquire the stream's single-consumer inbound receiver.
206 ///
207 /// The default delegates to [`Self::frames_in`] so third-party stream
208 /// implementations remain source compatible. Built-in transports
209 /// override this method and return [`crate::error::RvoipError::InvalidState`]
210 /// when ownership has already been transferred.
211 fn try_frames_in(&self) -> Result<mpsc::Receiver<MediaFrame>> {
212 Ok(self.frames_in())
213 }
214
215 /// Provisionally reserve the stream's single-consumer inbound receiver.
216 ///
217 /// The default preserves source compatibility for third-party stream
218 /// implementations but fails before acquiring anything. Streams used by
219 /// atomic multi-source orchestration should override this method and
220 /// return a rollback-capable [`MediaReceiverReservation`]. Legacy callers
221 /// may continue to use [`Self::try_frames_in`].
222 fn reserve_frames_in(&self) -> Result<MediaReceiverReservation> {
223 Err(crate::error::RvoipError::NotImplemented(
224 "MediaStream::reserve_frames_in",
225 ))
226 }
227 /// Obtain the legacy outbound-frame sender.
228 ///
229 /// This infallible API is retained for source compatibility. Streams whose
230 /// lifecycle can reject writes before activation should return a closed
231 /// sender in that state and override [`Self::try_frames_out`] so new callers
232 /// receive a typed error instead of discovering the rejection on `send`.
233 fn frames_out(&self) -> mpsc::Sender<MediaFrame>;
234
235 /// Fallibly obtain the outbound-frame sender.
236 ///
237 /// The default delegates to [`Self::frames_out`] so third-party stream
238 /// implementations remain source compatible. Deferred transports override
239 /// this method and return [`crate::error::RvoipError::InvalidState`] until
240 /// their media path has been activated.
241 fn try_frames_out(&self) -> Result<mpsc::Sender<MediaFrame>> {
242 Ok(self.frames_out())
243 }
244
245 fn quality_snapshot(&self) -> QualitySnapshot;
246
247 async fn close(self: Arc<Self>) -> Result<()>;
248}
249
250/// Cheap, cloneable reference a `crate::Connection` holds to its media flows.
251/// Wraps an `Arc<dyn MediaStream>` so the trait object can live in `Debug`
252/// types like `Connection` without forcing every adapter to implement Debug.
253#[derive(Clone)]
254pub struct MediaStreamHandle(pub Arc<dyn MediaStream>);
255
256impl MediaStreamHandle {
257 pub fn new(stream: Arc<dyn MediaStream>) -> Self {
258 Self(stream)
259 }
260
261 pub fn id(&self) -> StreamId {
262 self.0.id()
263 }
264
265 pub fn kind(&self) -> StreamKind {
266 self.0.kind()
267 }
268
269 pub fn stream(&self) -> &Arc<dyn MediaStream> {
270 &self.0
271 }
272}
273
274impl fmt::Debug for MediaStreamHandle {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 f.debug_struct("MediaStreamHandle")
277 .field("id", &self.0.id())
278 .field("kind", &self.0.kind())
279 .finish()
280 }
281}
282
283#[cfg(test)]
284mod diagnostic_tests {
285 use super::*;
286
287 #[test]
288 fn media_frame_debug_reports_shape_without_packet_bytes() {
289 const CANARY: &[u8] = b"media-frame-canary\r\nAuthorization: exposed";
290 let frame = MediaFrame {
291 stream_id: StreamId::from_string("stream-canary"),
292 kind: StreamKind::Audio,
293 payload: Bytes::from_static(CANARY),
294 timestamp_rtp: 123,
295 captured_at: Utc::now(),
296 payload_type: Some(111),
297 };
298 let debug = format!("{frame:?}");
299 assert!(!debug.contains("media-frame-canary"));
300 assert!(!debug.contains("stream-canary"));
301 assert!(debug.contains(&format!("payload_bytes: {}", CANARY.len())));
302 }
303}