Skip to main content

arcly_stream/bus/
application.rs

1//! One registered application and its live streams.
2
3use super::events::{StreamEvent, StreamEventKind};
4use super::handle::{StreamHandle, StreamState};
5use crate::observe::Observer;
6use crate::{AppName, Result, StreamError, StreamId};
7use dashmap::DashMap;
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::Arc;
10use tokio::sync::broadcast;
11use tracing::info;
12
13/// One registered application (e.g. "live", "vod").
14///
15/// Decoupled from `stream-center`'s `sc-config::AppConfig`: it takes a plain
16/// `broadcast_capacity` and an injected [`Observer`] instead of reaching for a
17/// metrics singleton.
18pub struct Application {
19    /// Application name (e.g. `"live"`).
20    pub name: AppName,
21    /// Per-stream broadcast channel capacity.
22    pub broadcast_capacity: usize,
23    /// Keyframe-anchored GOP replay buffer size, in frames (0 disables it).
24    pub gop_capacity: usize,
25    /// GOP replay-buffer payload byte cap (0 = unbounded).
26    pub gop_byte_capacity: usize,
27    /// Default cumulative-lag eviction budget for resilient subscribers.
28    pub subscriber_max_lag: Option<u64>,
29    streams: DashMap<StreamId, StreamHandle>,
30    /// Broadcast channel for stream lifecycle events within this application.
31    event_tx: broadcast::Sender<StreamEvent>,
32    /// Serializes concurrent start_publish calls to eliminate the check-then-insert
33    /// race condition (TOCTOU) that could allow two publishers to claim the same stream.
34    pub_lock: tokio::sync::Mutex<()>,
35    /// Lock-free count of active streams; updated on start/end publish.
36    stream_count: AtomicUsize,
37    /// Injected telemetry hook (no-op by default).
38    observer: Arc<dyn Observer>,
39}
40
41impl Application {
42    /// Create an application with the given per-stream capacities and observer.
43    pub fn new(
44        name: AppName,
45        broadcast_capacity: usize,
46        gop_capacity: usize,
47        gop_byte_capacity: usize,
48        subscriber_max_lag: Option<u64>,
49        observer: Arc<dyn Observer>,
50    ) -> Arc<Self> {
51        let (event_tx, _) = broadcast::channel(64);
52        Arc::new(Self {
53            name,
54            broadcast_capacity,
55            gop_capacity,
56            gop_byte_capacity,
57            subscriber_max_lag,
58            streams: DashMap::new(),
59            event_tx,
60            pub_lock: tokio::sync::Mutex::new(()),
61            stream_count: AtomicUsize::new(0),
62            observer,
63        })
64    }
65
66    /// Register a new publishing stream.  Fails if a stream with the same ID
67    /// is already in the `Publishing` or `Transcoding` state.
68    pub async fn start_publish(&self, stream_id: StreamId) -> Result<StreamHandle> {
69        // Hold this mutex for the duration of the check+insert to prevent two
70        // concurrent callers from both seeing the stream as free and both registering.
71        let _guard = self.pub_lock.lock().await;
72
73        if let Some(existing) = self.streams.get(&stream_id) {
74            let state = existing.current_state().await;
75            if matches!(state, StreamState::Publishing | StreamState::Transcoding) {
76                return Err(StreamError::StreamAlreadyPublishing {
77                    app: self.name.to_string(),
78                    stream_id: stream_id.to_string(),
79                });
80            }
81        }
82
83        let handle = StreamHandle::with_config(
84            self.name.clone(),
85            stream_id.clone(),
86            self.broadcast_capacity,
87            self.gop_capacity,
88            self.gop_byte_capacity,
89            self.subscriber_max_lag,
90            Arc::clone(&self.observer),
91        );
92        handle.set_state(StreamState::Publishing).await;
93        let started_at_ms = super::handle::now_ms();
94        handle
95            .update_metadata(|m| m.started_at_ms = started_at_ms)
96            .await;
97        self.streams.insert(stream_id.clone(), handle.clone());
98        self.stream_count.fetch_add(1, Ordering::Relaxed);
99        // Release the lock before notifying the observer to avoid holding it
100        // across a potentially-blocking host callback.
101        drop(_guard);
102        self.observer.on_publish_started(self.name.as_str());
103
104        info!(app = %self.name, stream = %stream_id, "Stream publish started");
105        self.emit(stream_id.clone(), StreamEventKind::PublishStarted);
106
107        Ok(handle)
108    }
109
110    /// Mark a stream as ended and remove it from the active registry.
111    /// Returns `true` if the stream was present and removed, `false` if it
112    /// was already gone (idempotent).
113    pub async fn end_publish(&self, stream_id: &StreamId) -> Result<bool> {
114        if let Some((_, handle)) = self.streams.remove(stream_id) {
115            self.stream_count.fetch_sub(1, Ordering::Relaxed);
116            self.observer.on_publish_ended(self.name.as_str());
117            handle.set_state(StreamState::Ended).await;
118            // Drop the sole frame-bus sender so every subscriber's `recv`
119            // terminates with `Closed`, even if some consumer still holds a
120            // handle clone (e.g. a WHEP egress pump).
121            handle.close();
122            info!(app = %self.name, stream = %stream_id, "Stream publish ended");
123            self.emit(stream_id.clone(), StreamEventKind::PublishEnded);
124            return Ok(true);
125        }
126        Ok(false)
127    }
128
129    /// Look up a live stream handle for playback.
130    pub fn get_stream(&self, stream_id: &StreamId) -> Option<StreamHandle> {
131        self.streams.get(stream_id).map(|r| r.clone())
132    }
133
134    /// List all active stream IDs.
135    pub fn active_streams(&self) -> Vec<StreamId> {
136        self.streams.iter().map(|r| r.key().clone()).collect()
137    }
138
139    /// Snapshot of all live stream handles (used by the engine's idle reaper).
140    pub fn active_handles(&self) -> Vec<StreamHandle> {
141        self.streams.iter().map(|r| r.value().clone()).collect()
142    }
143
144    /// Subscribe to stream lifecycle events for this application.
145    pub fn subscribe_events(&self) -> broadcast::Receiver<StreamEvent> {
146        self.event_tx.subscribe()
147    }
148
149    /// Number of streams currently active in this application.
150    pub fn stream_count(&self) -> usize {
151        self.stream_count.load(Ordering::Relaxed)
152    }
153
154    /// Emit a lifecycle event to both the broadcast channel and the observer.
155    fn emit(&self, stream_id: StreamId, kind: StreamEventKind) {
156        let event = StreamEvent {
157            app: self.name.clone(),
158            stream_id,
159            kind,
160        };
161        self.observer.on_event(&event);
162        let _ = self.event_tx.send(event);
163    }
164}
165
166impl std::fmt::Debug for Application {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("Application")
169            .field("name", &self.name)
170            .field("stream_count", &self.streams.len())
171            .finish()
172    }
173}