Skip to main content

o_sfu_core/engine/source_model/
descriptor.rs

1use itertools::Itertools;
2use o_sfu_rfc::rtp::{Mid, Rid, Ssrc};
3use o_sfu_router::{MediaKind, rtp::MediaFormat};
4use thiserror::Error;
5
6use super::{
7    PublishedSourceId, PublishedSourceOwner, SourceEncodingId, SourcePolicy, UploadLayerPolicyRole,
8    UserStreamId,
9};
10use crate::Bitrate;
11
12/// Rejection returned while assembling a source descriptor.
13///
14/// # Error handling
15///
16/// These are construction-time domain errors. They should be handled before a
17/// publish becomes authoritative in room state. They are not transport
18/// failures and should not be retried without rebuilding the source descriptor
19/// from valid runtime facts
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
21pub enum SourceModelError {
22    #[error("published source {source_id} has no advertised encoding")]
23    SourceWithoutEncodings { source_id: PublishedSourceId },
24    #[error("published source {source_id} has duplicate encoding {encoding_id}")]
25    DuplicateEncodingId {
26        source_id: PublishedSourceId,
27        encoding_id: SourceEncodingId,
28    },
29    #[error(
30        "encoding {encoding_id} belongs to {encoding_source_id}, not published source {source_id}"
31    )]
32    EncodingSourceMismatch {
33        source_id: PublishedSourceId,
34        encoding_id: SourceEncodingId,
35        encoding_source_id: PublishedSourceId,
36    },
37}
38
39/// Authoritative room-domain description of one published source.
40///
41/// The descriptor groups the stable source id, logical publishing user, caller
42/// stream id, media kind, source policy and negotiated source facts required by
43/// recording, diagnostics and transport projection. It deliberately excludes
44/// router producer state, socket state and packet-loop routing tables.
45///
46/// # Invariants
47///
48/// A descriptor must contain at least one encoding, every encoding id must be
49/// unique and every encoding must point back to this descriptor's source id.
50/// [`Self::new`] validates these rules so callers do not keep a parallel
51/// identity model by accident.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct PublishedSourceDescriptor {
54    /// Runtime source identity shared by every view of this publication.
55    source_id: PublishedSourceId,
56    /// Logical publishing user used by room policy and owner indexes.
57    ///
58    /// Stale connection checks use the source transport session key because
59    /// `PublishedSourceOwner` does not carry a `ConnectionId`.
60    owner: PublishedSourceOwner,
61    /// User-scoped stream identity supplied with the publish intent.
62    stream_id: UserStreamId,
63    /// Router-facing media family used by negotiation and route planning.
64    media_kind: MediaKind,
65    /// Room policy captured by the publish intent for this source.
66    policy: SourcePolicy,
67    /// Negotiated media-section identity when the RTC edge has one.
68    mid: Option<Mid>,
69    /// Advertised encodings that belong to this logical source.
70    encodings: Vec<SourceEncodingDescriptor>,
71    /// Source-policy selectable encodings ordered by receiver budget priority.
72    selectable_encoding_indices: Vec<usize>,
73}
74
75impl PublishedSourceDescriptor {
76    /// Builds a source descriptor after checking the source graph invariants.
77    ///
78    /// Failure means the caller assembled an invalid room-domain source and
79    /// should abort the surrounding publish commit before any registry state is
80    /// made authoritative.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`SourceModelError`] when the descriptor has no encodings, uses
85    /// a duplicate encoding id or contains an encoding whose source id points
86    /// elsewhere
87    pub fn new(parts: PublishedSourceDescriptorParts) -> Result<Self, SourceModelError> {
88        if parts.encodings.is_empty() {
89            return Err(SourceModelError::SourceWithoutEncodings {
90                source_id: parts.source_id,
91            });
92        }
93        if let Some(encoding) = parts
94            .encodings
95            .iter()
96            .find(|encoding| encoding.source_id() != parts.source_id)
97        {
98            return Err(SourceModelError::EncodingSourceMismatch {
99                source_id: parts.source_id,
100                encoding_id: encoding.encoding_id(),
101                encoding_source_id: encoding.source_id(),
102            });
103        }
104        if let Some(encoding_id) = duplicate_encoding_id(&parts.encodings) {
105            return Err(SourceModelError::DuplicateEncodingId {
106                source_id: parts.source_id,
107                encoding_id,
108            });
109        }
110        let selectable_encoding_indices = selectable_encoding_indices(&parts.encodings);
111        Ok(Self {
112            source_id: parts.source_id,
113            owner: parts.owner,
114            stream_id: parts.stream_id,
115            media_kind: parts.media_kind,
116            policy: parts.policy,
117            mid: parts.mid,
118            encodings: parts.encodings,
119            selectable_encoding_indices,
120        })
121    }
122
123    #[must_use]
124    pub const fn source_id(&self) -> PublishedSourceId {
125        self.source_id
126    }
127
128    #[must_use]
129    pub const fn owner(&self) -> &PublishedSourceOwner {
130        &self.owner
131    }
132
133    #[must_use]
134    pub const fn stream_id(&self) -> &UserStreamId {
135        &self.stream_id
136    }
137
138    #[must_use]
139    pub const fn media_kind(&self) -> MediaKind {
140        self.media_kind
141    }
142
143    #[must_use]
144    pub const fn policy(&self) -> SourcePolicy {
145        self.policy
146    }
147
148    #[must_use]
149    pub fn mid(&self) -> Option<&Mid> {
150        self.mid.as_ref()
151    }
152
153    pub fn encodings(&self) -> impl Iterator<Item = &SourceEncodingDescriptor> {
154        self.encodings.iter()
155    }
156
157    /// Returns encodings in receiver-policy rank order.
158    ///
159    /// Selection is disabled unless every encoding has a RID because packet-gate
160    /// projection cannot represent the full ladder otherwise. If any maximum
161    /// bitrate is known, known values sort first in ascending order. Otherwise
162    /// known upload roles sort first from `DegradedThumbnail` to `Featured`.
163    /// Missing sort keys and ties preserve publisher order.
164    pub fn selectable_encodings(&self) -> impl Iterator<Item = &SourceEncodingDescriptor> {
165        self.selectable_encoding_indices
166            .iter()
167            .filter_map(|index| self.encodings.get(*index))
168    }
169
170    #[must_use]
171    pub fn selectable_encoding_count(&self) -> usize {
172        self.selectable_encoding_indices.len()
173    }
174
175    #[must_use]
176    pub fn selectable_encoding_by_rank(&self, rank: usize) -> Option<&SourceEncodingDescriptor> {
177        self.selectable_encoding_indices
178            .get(rank)
179            .and_then(|index| self.encodings.get(*index))
180    }
181
182    /// Returns an encoding by source-encoding identity.
183    ///
184    /// Missing values are normal for best-effort callers such as diagnostics or
185    /// selector resolution after a source changed. Mutation paths should treat a
186    /// miss as stale work and re-read authoritative room state.
187    #[must_use]
188    pub fn encoding(&self, encoding_id: SourceEncodingId) -> Option<&SourceEncodingDescriptor> {
189        self.encodings
190            .iter()
191            .find(|encoding| encoding.encoding_id() == encoding_id)
192    }
193}
194
195fn duplicate_encoding_id(encodings: &[SourceEncodingDescriptor]) -> Option<SourceEncodingId> {
196    encodings
197        .iter()
198        .tuple_combinations()
199        .find_map(|(left, right)| {
200            (left.encoding_id() == right.encoding_id()).then_some(left.encoding_id())
201        })
202}
203
204fn selectable_encoding_indices(encodings: &[SourceEncodingDescriptor]) -> Vec<usize> {
205    if encodings.iter().any(|encoding| encoding.rid().is_none()) {
206        return Vec::new();
207    }
208    let mut indices = (0..encodings.len()).collect::<Vec<_>>();
209    if encodings
210        .iter()
211        .any(|encoding| encoding.max_bitrate().is_some())
212    {
213        indices.sort_by_key(|index| {
214            encodings
215                .get(*index)
216                .and_then(SourceEncodingDescriptor::max_bitrate)
217                .unwrap_or(Bitrate::from_bps(u64::MAX))
218        });
219    } else if encodings
220        .iter()
221        .any(|encoding| encoding.policy_role().is_some())
222    {
223        indices.sort_by_key(|index| {
224            encodings
225                .get(*index)
226                .and_then(SourceEncodingDescriptor::policy_role)
227                .map_or(u8::MAX, upload_layer_policy_role_rank)
228        });
229    }
230    indices
231}
232
233// Keep policy roles low-to-high so rank 0 has the same meaning as in the
234// ascending-bitrate path.
235const fn upload_layer_policy_role_rank(role: UploadLayerPolicyRole) -> u8 {
236    match role {
237        UploadLayerPolicyRole::DegradedThumbnail => 0,
238        UploadLayerPolicyRole::Thumbnail => 1,
239        UploadLayerPolicyRole::Featured => 2,
240    }
241}
242
243/// Construction input for [`PublishedSourceDescriptor`].
244///
245/// The grouped input keeps descriptor construction explicit without growing a
246/// long positional constructor. Callers should fill it from already-normalized
247/// runtime facts, not raw SDP or browser JSON
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct PublishedSourceDescriptorParts {
250    /// Stable source id allocated by the room-domain registry.
251    pub source_id: PublishedSourceId,
252    /// Logical publishing user. Connection identity is carried separately by
253    /// `TransportSessionKey`.
254    pub owner: PublishedSourceOwner,
255    /// User-scoped stream identity supplied with the publish intent.
256    pub stream_id: UserStreamId,
257    /// Router-facing media family for this source.
258    pub media_kind: MediaKind,
259    /// Room policy captured by the publish intent for this source.
260    pub policy: SourcePolicy,
261    /// Negotiated media-section id when known.
262    pub mid: Option<Mid>,
263    /// Encodings that belong to this source.
264    pub encodings: Vec<SourceEncodingDescriptor>,
265}
266
267/// Room-domain description of one advertised source encoding.
268///
269/// This keeps policy identity separate from negotiated transport facts. RID,
270/// SSRC, bitrate and codec data are metadata that help route projection,
271/// diagnostics and recording describe the encoding. They are not substitutes
272/// for [`SourceEncodingId`].
273#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct SourceEncodingDescriptor {
275    /// Stable source-encoding identity used by selectors.
276    encoding_id: SourceEncodingId,
277    /// Parent logical source.
278    source_id: PublishedSourceId,
279    /// Negotiated RID when simulcast or layered transport exposes one.
280    rid: Option<Rid>,
281    /// Primary RTP SSRC when known from negotiation or packet observation.
282    primary_ssrc: Option<Ssrc>,
283    /// Repair RTP SSRC such as RTX when known.
284    repair_ssrc: Option<Ssrc>,
285    /// Sender-declared bitrate ceiling for this encoding.
286    max_bitrate: Option<Bitrate>,
287    /// Sender-side resolution downscale advertised for this encoding.
288    resolution_scale: Option<u16>,
289    /// Sender-side frame-rate ceiling advertised for this encoding.
290    max_framerate: Option<u16>,
291    /// Server-defined policy role associated with this encoding.
292    policy_role: Option<UploadLayerPolicyRole>,
293    /// Negotiated payload and codec information for this encoding.
294    negotiated_format: Option<MediaFormat>,
295}
296
297impl SourceEncodingDescriptor {
298    /// Creates an encoding descriptor from normalized runtime facts.
299    ///
300    /// This constructor does not validate parent membership because a single
301    /// encoding is not authoritative alone. [`PublishedSourceDescriptor::new`]
302    /// validates the full source graph when the encoding list is assembled.
303    #[must_use]
304    pub fn new(parts: SourceEncodingDescriptorParts) -> Self {
305        Self {
306            encoding_id: parts.encoding_id,
307            source_id: parts.source_id,
308            rid: parts.rid,
309            primary_ssrc: parts.primary_ssrc,
310            repair_ssrc: parts.repair_ssrc,
311            max_bitrate: parts.max_bitrate,
312            resolution_scale: parts.resolution_scale,
313            max_framerate: parts.max_framerate,
314            policy_role: parts.policy_role,
315            negotiated_format: parts.negotiated_format,
316        }
317    }
318
319    #[must_use]
320    pub const fn encoding_id(&self) -> SourceEncodingId {
321        self.encoding_id
322    }
323
324    #[must_use]
325    pub const fn source_id(&self) -> PublishedSourceId {
326        self.source_id
327    }
328
329    #[must_use]
330    pub fn rid(&self) -> Option<&Rid> {
331        self.rid.as_ref()
332    }
333
334    #[must_use]
335    pub const fn primary_ssrc(&self) -> Option<Ssrc> {
336        self.primary_ssrc
337    }
338
339    #[must_use]
340    pub const fn repair_ssrc(&self) -> Option<Ssrc> {
341        self.repair_ssrc
342    }
343
344    #[must_use]
345    pub const fn max_bitrate(&self) -> Option<Bitrate> {
346        self.max_bitrate
347    }
348
349    #[must_use]
350    pub const fn resolution_scale(&self) -> Option<u16> {
351        self.resolution_scale
352    }
353
354    #[must_use]
355    pub const fn max_framerate(&self) -> Option<u16> {
356        self.max_framerate
357    }
358
359    #[must_use]
360    pub const fn policy_role(&self) -> Option<UploadLayerPolicyRole> {
361        self.policy_role
362    }
363
364    #[must_use]
365    pub fn negotiated_format(&self) -> Option<&MediaFormat> {
366        self.negotiated_format.as_ref()
367    }
368}
369
370/// Construction input for [`SourceEncodingDescriptor`]
371///
372/// The fields are optional where negotiation may not have learned the fact yet.
373/// The source and encoding ids must still be stable before the descriptor is
374/// stored in room state
375#[derive(Debug, Clone, PartialEq, Eq)]
376pub struct SourceEncodingDescriptorParts {
377    /// Stable encoding id allocated by the room-domain registry.
378    pub encoding_id: SourceEncodingId,
379    /// Parent source id.
380    pub source_id: PublishedSourceId,
381    /// Negotiated RID for RID-based simulcast.
382    pub rid: Option<Rid>,
383    /// Primary RTP SSRC when available.
384    pub primary_ssrc: Option<Ssrc>,
385    /// Repair RTP SSRC when available.
386    pub repair_ssrc: Option<Ssrc>,
387    /// Optional bitrate ceiling advertised for this encoding.
388    pub max_bitrate: Option<Bitrate>,
389    /// Optional resolution downscale advertised for this encoding.
390    pub resolution_scale: Option<u16>,
391    /// Optional frame-rate ceiling advertised for this encoding.
392    pub max_framerate: Option<u16>,
393    /// Optional policy role advertised for this encoding.
394    pub policy_role: Option<UploadLayerPolicyRole>,
395    /// Negotiated codec and payload information when available.
396    pub negotiated_format: Option<MediaFormat>,
397}