arcly_stream/traits.rs
1//! The foundational extension traits a downstream user implements.
2//!
3//! These are codec- and runtime-agnostic. Implement the ones you need and the
4//! engine drives them; the engine never assumes a particular protocol,
5//! container, or storage backend.
6
7use crate::bus::PublishRegistry;
8use crate::frame::CodecId;
9use crate::{MediaFrame, Result, StreamError};
10use async_trait::async_trait;
11use bytes::Bytes;
12use std::path::Path;
13use std::sync::Arc;
14
15/// A source that produces [`MediaFrame`]s (e.g. a protocol ingest connection).
16#[async_trait]
17pub trait MediaSource: Send + Sync {
18 /// Returns the next frame, or `Ok(None)` when the source is exhausted.
19 /// An `Err` result indicates a fatal I/O or protocol error.
20 ///
21 /// Using `Result<Option<T>>` rather than `Option<Result<T>>` follows the
22 /// standard Rust I/O convention: `Ok(Some(frame))` = got a frame,
23 /// `Ok(None)` = end of stream, `Err(e)` = unrecoverable error.
24 async fn next_frame(&mut self) -> Result<Option<MediaFrame>>;
25}
26
27/// A sink that consumes [`MediaFrame`]s (e.g. a protocol playback connection,
28/// a transcoding pipeline node, or a recorder).
29#[async_trait]
30pub trait MediaSink: Send + Sync {
31 /// Send a frame to this sink.
32 async fn send_frame(&mut self, frame: MediaFrame) -> Result<()>;
33
34 /// Flush any internal buffers and signal end of stream.
35 async fn flush(&mut self) -> Result<()> {
36 Ok(())
37 }
38}
39
40/// Adapts a [`MediaSink`] to the bus's [`FrameSink`](crate::bus::FrameSink) so it
41/// can be driven directly by
42/// [`StreamHandle::drive_to`](crate::bus::StreamHandle::drive_to) — the one-call
43/// path to **record or restream any live stream** (a WebRTC ingest included) to a
44/// sink, with the instant-start GOP replay the bus already provides.
45///
46/// ```ignore
47/// let handle = playback.get_stream(&key)?;
48/// let mut driver = SinkDriver::new(RecordingSink::new(storage, "rec/cam", 2));
49/// handle.drive_to(&shutdown, &mut driver).await?;
50/// driver.into_inner().flush().await?; // finalize the last chunk
51/// ```
52pub struct SinkDriver<S: MediaSink>(S);
53
54impl<S: MediaSink> SinkDriver<S> {
55 /// Wrap a [`MediaSink`] for use with `drive_to`.
56 pub fn new(sink: S) -> Self {
57 Self(sink)
58 }
59
60 /// Recover the inner sink (e.g. to `flush` it after the drive ends).
61 pub fn into_inner(self) -> S {
62 self.0
63 }
64}
65
66#[async_trait]
67impl<S: MediaSink> crate::bus::FrameSink for SinkDriver<S> {
68 async fn send(&mut self, frame: Arc<MediaFrame>) -> Result<std::ops::ControlFlow<()>> {
69 // The bus hands a shared `Arc`; `MediaSink` takes ownership. Avoid a copy
70 // when this is the last reference, else clone (cheap — payload is `Bytes`).
71 let frame = Arc::try_unwrap(frame).unwrap_or_else(|a| (*a).clone());
72 self.0.send_frame(frame).await?;
73 Ok(std::ops::ControlFlow::Continue(()))
74 }
75}
76
77/// Low-level protocol handler that accepts connections and maps them to
78/// stream identities. Each concrete protocol implementation provides this.
79///
80/// Unlike `stream-center`'s original `ProtocolHandler`, `run` receives a
81/// [`PublishRegistry`] trait object rather than reaching for a concrete
82/// `ApplicationRegistry`, so a handler is reusable against any engine — or a
83/// host's own bus implementation.
84#[async_trait]
85pub trait ProtocolHandler: Send + Sync {
86 /// Human-readable name (e.g. `"rtmp"`, `"hls"`).
87 fn name(&self) -> &'static str;
88
89 /// Start accepting connections. Implementations should run until the
90 /// provided `shutdown` token is cancelled, resolving incoming connections
91 /// to a [`StreamKey`](crate::StreamKey) and forwarding frames through
92 /// `registry`.
93 async fn run(
94 &self,
95 registry: Arc<dyn PublishRegistry>,
96 shutdown: tokio_util::sync::CancellationToken,
97 ) -> Result<()>;
98}
99
100/// Object-storage abstraction used by recording and HLS/DASH packagers.
101#[async_trait]
102pub trait StorageBackend: Send + Sync + 'static {
103 /// Write `data` at `key`. Overwrites if the key exists.
104 async fn put(&self, key: &str, data: Bytes) -> Result<()>;
105
106 /// Read the object stored at `key`.
107 async fn get(&self, key: &str) -> Result<Bytes>;
108
109 /// Delete the object at `key`.
110 async fn delete(&self, key: &str) -> Result<()>;
111
112 /// List all keys with the given `prefix`.
113 async fn list(&self, prefix: &str) -> Result<Vec<String>>;
114
115 /// Returns `true` if the object exists.
116 async fn exists(&self, key: &str) -> Result<bool> {
117 match self.get(key).await {
118 Ok(_) => Ok(true),
119 Err(StreamError::StorageNotFound(_)) => Ok(false),
120 Err(e) => Err(e),
121 }
122 }
123
124 /// Write the file at `path` to `key`, then remove the source file.
125 ///
126 /// The default implementation reads the file into memory and calls [`put`](Self::put).
127 /// Backends that share a filesystem with the temp directory (e.g. disk)
128 /// should override this with a rename to avoid the memory round-trip.
129 async fn put_file(&self, key: &str, path: &Path) -> Result<()> {
130 let data = tokio::fs::read(path)
131 .await
132 .map_err(|e| StreamError::storage(format!("put_file read failed: {e}")))?;
133 self.put(key, Bytes::from(data)).await?;
134 tokio::fs::remove_file(path).await.ok();
135 Ok(())
136 }
137}
138
139/// `Arc<B>` is itself a [`StorageBackend`], so a single backend can be shared
140/// across many packagers/recorders without wrapping boilerplate.
141#[async_trait]
142impl<B: StorageBackend> StorageBackend for Arc<B> {
143 async fn put(&self, key: &str, data: Bytes) -> Result<()> {
144 (**self).put(key, data).await
145 }
146 async fn get(&self, key: &str) -> Result<Bytes> {
147 (**self).get(key).await
148 }
149 async fn delete(&self, key: &str) -> Result<()> {
150 (**self).delete(key).await
151 }
152 async fn list(&self, prefix: &str) -> Result<Vec<String>> {
153 (**self).list(prefix).await
154 }
155 async fn exists(&self, key: &str) -> Result<bool> {
156 (**self).exists(key).await
157 }
158 async fn put_file(&self, key: &str, path: &Path) -> Result<()> {
159 (**self).put_file(key, path).await
160 }
161}
162
163/// Hardware-acceleration backend (NVENC, AMF, QSV, NPU, …).
164///
165/// Implementors are registered at startup based on available hardware.
166pub trait HwAccelBackend: Send + Sync {
167 /// Unique backend identifier (e.g. `"nvenc"`, `"qsv"`).
168 fn id(&self) -> &'static str;
169
170 /// Returns whether the backend is usable on this machine.
171 fn is_available(&self) -> bool;
172
173 /// List codec IDs this backend can encode.
174 fn supported_encoders(&self) -> &[CodecId];
175
176 /// List codec IDs this backend can decode.
177 fn supported_decoders(&self) -> &[CodecId];
178}