Skip to main content

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}