use crate::bus::StreamHandle;
use crate::{MediaFrame, Result, StreamKey};
use async_trait::async_trait;
use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct NodeAddr(pub String);
#[async_trait]
pub trait FrameSource: Send {
fn replay(&mut self) -> Vec<MediaFrame> {
Vec::new()
}
async fn next(&mut self) -> Option<MediaFrame>;
}
pub async fn mirror<S: FrameSource>(mut src: S, dst: &StreamHandle, cancel: &CancellationToken) {
for frame in src.replay() {
if dst.publish_frame(frame).is_err() {
return; }
}
loop {
tokio::select! {
_ = cancel.cancelled() => return,
frame = src.next() => match frame {
Some(frame) => {
if dst.publish_frame(frame).is_err() {
return;
}
}
None => return, },
}
}
}
impl<T: Into<String>> From<T> for NodeAddr {
fn from(s: T) -> Self {
NodeAddr(s.into())
}
}
#[async_trait]
pub trait ClusterRelay: Send + Sync + 'static {
async fn locate(&self, key: &StreamKey) -> Result<Option<NodeAddr>>;
async fn pull(&self, key: &StreamKey, origin: &NodeAddr) -> Result<()>;
async fn announce(&self, key: &StreamKey) -> Result<()>;
async fn withdraw(&self, key: &StreamKey) -> Result<()>;
async fn ensure_mirrored(&self, key: &StreamKey) -> Result<bool> {
match self.locate(key).await? {
Some(origin) => {
self.pull(key, &origin).await?;
Ok(true)
}
None => Ok(false),
}
}
}
#[cfg(feature = "cluster")]
pub use relay::{ClusterDirectory, InProcessRelay};
#[cfg(feature = "cluster")]
mod relay {
use super::{ClusterRelay, FrameSource, NodeAddr};
use crate::bus::{PlaybackRegistry, PublishRegistry, StreamHandle, Subscription};
use crate::{MediaFrame, Result, StreamError, StreamKey};
use async_trait::async_trait;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tokio_util::sync::CancellationToken;
struct HandleSource {
replay: Vec<MediaFrame>,
sub: Subscription,
}
impl HandleSource {
fn new(handle: StreamHandle) -> Self {
let replay = handle
.replay_buffer()
.into_iter()
.map(|f| (*f).clone())
.collect();
let sub = handle.subscribe_resilient();
Self { replay, sub }
}
}
#[async_trait]
impl FrameSource for HandleSource {
fn replay(&mut self) -> Vec<MediaFrame> {
std::mem::take(&mut self.replay)
}
async fn next(&mut self) -> Option<MediaFrame> {
self.sub.recv().await.map(|f| (*f).clone())
}
}
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
pub struct ClusterDirectory {
serving: Mutex<HashMap<StreamKey, HashMap<NodeAddr, Option<Instant>>>>,
ttl: Option<Duration>,
}
impl ClusterDirectory {
pub fn new() -> Self {
Self::default()
}
pub fn with_ttl(ttl: Duration) -> Self {
Self {
serving: Mutex::new(HashMap::new()),
ttl: Some(ttl),
}
}
pub fn announce(&self, node: &NodeAddr, key: &StreamKey) {
let expires = self.ttl.map(|t| Instant::now() + t);
self.serving
.lock()
.unwrap()
.entry(key.clone())
.or_default()
.insert(node.clone(), expires);
}
pub fn withdraw(&self, node: &NodeAddr, key: &StreamKey) {
if let Some(set) = self.serving.lock().unwrap().get_mut(key) {
set.remove(node);
}
}
pub fn locate(&self, key: &StreamKey, exclude: &NodeAddr) -> Option<NodeAddr> {
let now = Instant::now();
let mut map = self.serving.lock().unwrap();
let set = map.get_mut(key)?;
set.retain(|_, exp| exp.is_none_or(|e| e > now)); set.keys().find(|n| *n != exclude).cloned()
}
}
pub struct InProcessRelay {
node: NodeAddr,
local: Arc<dyn PublishRegistry>,
directory: Arc<ClusterDirectory>,
peers: HashMap<NodeAddr, Arc<dyn PlaybackRegistry>>,
reconnect: bool,
mirrors: Mutex<HashMap<StreamKey, CancellationToken>>,
}
impl InProcessRelay {
pub fn new(
node: impl Into<NodeAddr>,
local: Arc<dyn PublishRegistry>,
directory: Arc<ClusterDirectory>,
) -> Self {
Self {
node: node.into(),
local,
directory,
peers: HashMap::new(),
reconnect: false,
mirrors: Mutex::new(HashMap::new()),
}
}
pub fn with_peer(
mut self,
addr: impl Into<NodeAddr>,
playback: Arc<dyn PlaybackRegistry>,
) -> Self {
self.peers.insert(addr.into(), playback);
self
}
pub fn with_reconnect(mut self, reconnect: bool) -> Self {
self.reconnect = reconnect;
self
}
}
#[async_trait]
impl ClusterRelay for InProcessRelay {
async fn locate(&self, key: &StreamKey) -> Result<Option<NodeAddr>> {
Ok(self.directory.locate(key, &self.node))
}
async fn pull(&self, key: &StreamKey, origin: &NodeAddr) -> Result<()> {
let peer = self.peers.get(origin).ok_or_else(|| {
StreamError::protocol(format!("cluster: unknown origin node {}", origin.0))
})?;
let src = peer.get_stream(key)?;
let dst = self.local.start_publish(key).await?;
let cancel = CancellationToken::new();
self.mirrors
.lock()
.unwrap()
.insert(key.clone(), cancel.clone());
let local = Arc::clone(&self.local);
let directory = Arc::clone(&self.directory);
let peers = self.peers.clone();
let reconnect = self.reconnect;
let key = key.clone();
let mut origin = origin.clone();
tokio::spawn(async move {
super::mirror(HandleSource::new(src), &dst, &cancel).await;
while reconnect && !cancel.is_cancelled() {
let Some(next) = directory.locate(&key, &origin) else {
break; };
origin = next;
let Some(peer) = peers.get(&origin) else {
break;
};
let Ok(src) = peer.get_stream(&key) else {
break;
};
super::mirror(HandleSource::new(src), &dst, &cancel).await;
}
let _ = local.end_publish(&key).await;
});
Ok(())
}
async fn announce(&self, key: &StreamKey) -> Result<()> {
self.directory.announce(&self.node, key);
Ok(())
}
async fn withdraw(&self, key: &StreamKey) -> Result<()> {
if let Some(cancel) = self.mirrors.lock().unwrap().remove(key) {
cancel.cancel(); }
self.directory.withdraw(&self.node, key);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AppSpec, CodecId, Engine, MediaFrame};
fn frame(pts: i64) -> MediaFrame {
MediaFrame::new_video(
pts,
pts,
bytes::Bytes::from_static(b"x"),
CodecId::H264,
true,
)
}
#[tokio::test]
async fn edge_locates_and_mirrors_origin_stream() {
let directory = Arc::new(ClusterDirectory::new());
let key = StreamKey::new("live", "cam");
let origin = Engine::builder()
.application(AppSpec::new("live").gop_cache(8))
.build();
let origin_relay = InProcessRelay::new("origin", origin.clone(), directory.clone());
let src_handle = origin.start_publish(&key).await.unwrap();
origin_relay.announce(&key).await.unwrap();
src_handle.publish_frame(frame(0)).unwrap();
let edge = Engine::builder()
.application(AppSpec::new("live").gop_cache(8))
.build();
let edge_relay = InProcessRelay::new("edge", edge.clone(), directory.clone())
.with_peer("origin", origin.clone());
assert!(edge.get_stream(&key).is_err(), "not local yet");
let found = edge_relay.locate(&key).await.unwrap();
assert_eq!(found, Some(NodeAddr::from("origin")));
edge_relay.pull(&key, &found.unwrap()).await.unwrap();
let mirror = edge.get_stream(&key).expect("local mirror exists");
let mut sub = mirror.subscribe_resilient();
src_handle.publish_frame(frame(1)).unwrap();
let got = tokio::time::timeout(std::time::Duration::from_secs(5), sub.recv())
.await
.expect("a frame was mirrored")
.expect("frame");
assert!(got.is_video());
}
#[tokio::test]
async fn locate_excludes_self_and_withdraw_clears() {
let directory = Arc::new(ClusterDirectory::new());
let key = StreamKey::new("live", "s");
let engine = Engine::builder().application(AppSpec::new("live")).build();
let relay = InProcessRelay::new("only", engine, directory.clone());
relay.announce(&key).await.unwrap();
assert_eq!(relay.locate(&key).await.unwrap(), None);
relay.withdraw(&key).await.unwrap();
assert!(directory.locate(&key, &NodeAddr::from("other")).is_none());
}
#[tokio::test]
async fn ttl_directory_reaps_an_unrenewed_lease() {
let dir = ClusterDirectory::with_ttl(Duration::from_millis(80));
let key = StreamKey::new("live", "cam");
dir.announce(&NodeAddr::from("origin"), &key);
assert_eq!(
dir.locate(&key, &NodeAddr::from("edge")),
Some(NodeAddr::from("origin"))
);
tokio::time::sleep(Duration::from_millis(160)).await;
assert_eq!(dir.locate(&key, &NodeAddr::from("edge")), None);
dir.announce(&NodeAddr::from("origin"), &key);
assert!(dir.locate(&key, &NodeAddr::from("edge")).is_some());
}
#[tokio::test]
async fn reconnect_fails_over_to_a_second_origin() {
let directory = Arc::new(ClusterDirectory::new());
let key = StreamKey::new("live", "cam");
let origin_a = Engine::builder()
.application(AppSpec::new("live").gop_cache(8))
.build();
let origin_b = Engine::builder()
.application(AppSpec::new("live").gop_cache(8))
.build();
let a = origin_a.start_publish(&key).await.unwrap();
let b = origin_b.start_publish(&key).await.unwrap();
directory.announce(&NodeAddr::from("A"), &key);
directory.announce(&NodeAddr::from("B"), &key);
a.publish_frame(frame(0)).unwrap();
b.publish_frame(frame(0)).unwrap();
let edge = Engine::builder()
.application(AppSpec::new("live").gop_cache(8))
.build();
let relay = InProcessRelay::new("edge", edge.clone(), directory.clone())
.with_peer("A", origin_a.clone())
.with_peer("B", origin_b.clone())
.with_reconnect(true);
relay.pull(&key, &NodeAddr::from("A")).await.unwrap();
let mirror = edge.get_stream(&key).expect("local mirror");
let mut sub = mirror.subscribe_resilient();
directory.withdraw(&NodeAddr::from("A"), &key);
origin_a.end_publish(&key).await.unwrap();
for n in 1..20 {
b.publish_frame(frame(n)).unwrap();
}
let got = tokio::time::timeout(std::time::Duration::from_secs(5), sub.recv())
.await
.expect("a frame arrived after failover")
.expect("frame");
assert!(got.is_video());
assert!(edge.get_stream(&key).is_ok(), "local handle stable");
}
}
}