Skip to main content

arcly_stream/
identity.rs

1//! Cheaply-cloneable identifiers for applications and streams.
2
3use std::fmt;
4use std::sync::Arc;
5
6/// Shared implementation for cheaply-cloneable `Arc<str>` newtype identifiers.
7macro_rules! arc_str_newtype {
8    ($(#[$meta:meta])* $Name:ident) => {
9        $(#[$meta])*
10        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
11        pub struct $Name(Arc<str>);
12
13        impl serde::Serialize for $Name {
14            fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
15                s.serialize_str(&self.0)
16            }
17        }
18
19        impl<'de> serde::Deserialize<'de> for $Name {
20            fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
21                let s = String::deserialize(d)?;
22                Ok(Self(Arc::from(s.as_str())))
23            }
24        }
25
26        impl $Name {
27            /// Create a new identifier from any string-like value.
28            pub fn new(s: impl Into<String>) -> Self {
29                Self(Arc::from(s.into().as_str()))
30            }
31
32            /// Borrow the identifier as a `&str`.
33            pub fn as_str(&self) -> &str {
34                &self.0
35            }
36        }
37
38        impl fmt::Display for $Name {
39            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40                f.write_str(&self.0)
41            }
42        }
43
44        impl From<&str> for $Name {
45            fn from(s: &str) -> Self { Self(Arc::from(s)) }
46        }
47
48        impl From<String> for $Name {
49            fn from(s: String) -> Self { Self(Arc::from(s.as_str())) }
50        }
51    };
52}
53
54arc_str_newtype!(
55    /// Name of an application (e.g. `"live"`, `"vod"`).
56    /// Cheaply cloneable — backed by an `Arc<str>`.
57    AppName
58);
59
60arc_str_newtype!(
61    /// Unique identifier for a stream within an application.
62    /// Cheaply cloneable — backed by an `Arc<str>`.
63    StreamId
64);
65
66/// Composite key uniquely identifying a stream: `(app_name, stream_id)`.
67#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
68pub struct StreamKey {
69    /// Application the stream belongs to.
70    pub app: AppName,
71    /// Stream identifier within the application.
72    pub stream_id: StreamId,
73}
74
75impl StreamKey {
76    /// Build a key from an application name and stream id.
77    pub fn new(app: impl Into<AppName>, stream_id: impl Into<StreamId>) -> Self {
78        Self {
79            app: app.into(),
80            stream_id: stream_id.into(),
81        }
82    }
83
84    /// The key for an adaptive-bitrate *layer* of this stream: `<stream>~<variant>`
85    /// in the same application.
86    ///
87    /// This is the one convention shared across the media plane — the WebRTC
88    /// simulcast demux (one key per RID), an ABR transcoder (one per rendition
89    /// rung), and the WHEP layer selector that discovers and switches between them
90    /// all agree on it, so a transcoded rung and a simulcast layer are
91    /// interchangeable to a viewer.
92    pub fn layer(&self, variant: &str) -> StreamKey {
93        StreamKey::new(
94            self.app.as_str(),
95            format!("{}~{}", self.stream_id.as_str(), variant),
96        )
97    }
98
99    /// The base stream id of a layer key (everything before the first `~`), i.e.
100    /// the inverse of [`layer`](Self::layer). Returns the whole id when it carries
101    /// no `~` (already a base stream).
102    pub fn layer_base(&self) -> &str {
103        let s = self.stream_id.as_str();
104        s.split_once('~').map(|(base, _)| base).unwrap_or(s)
105    }
106}
107
108impl fmt::Display for StreamKey {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        write!(f, "{}/{}", self.app, self.stream_id)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn newtypes_are_cheap_clones_with_value_equality() {
120        let a = AppName::from("live");
121        let b = a.clone();
122        assert_eq!(a, b);
123        assert_eq!(a.as_str(), "live");
124        // Clones share the same Arc allocation (cheap, not a deep copy).
125        assert!(std::ptr::eq(a.as_str().as_ptr(), b.as_str().as_ptr()));
126    }
127
128    #[test]
129    fn layer_key_round_trips_with_base() {
130        let base = StreamKey::new("live", "show");
131        let high = base.layer("f");
132        assert_eq!(high.stream_id.as_str(), "show~f");
133        assert_eq!(high.app, base.app);
134        // The base id is recoverable from a layer key, and a plain key is its own
135        // base.
136        assert_eq!(high.layer_base(), "show");
137        assert_eq!(base.layer_base(), "show");
138    }
139
140    #[test]
141    fn stream_key_display_is_app_slash_stream() {
142        let key = StreamKey::new("live", "cam-1");
143        assert_eq!(key.to_string(), "live/cam-1");
144    }
145
146    #[test]
147    fn identifiers_roundtrip_through_serde() {
148        let key = StreamKey::new("vod", "movie");
149        let json = serde_json::to_string(&key).expect("serialize");
150        let back: StreamKey = serde_json::from_str(&json).expect("deserialize");
151        assert_eq!(key, back);
152    }
153}