every.channel: sanitized baseline
This commit is contained in:
commit
897e556bea
258 changed files with 74298 additions and 0 deletions
19
third_party/iroh-live/iroh-moq/Cargo.toml
vendored
Normal file
19
third_party/iroh-live/iroh-moq/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "iroh-moq"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "audio and video live streaming over iroh"
|
||||
authors = ["Franz Heinzmann <frando@n0.computer>"]
|
||||
repository = "https://github.com/n0-computer/iroh-live"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
iroh = "0.96"
|
||||
moq-lite = "0.10.1"
|
||||
n0-error = { version = "0.1.2", features = ["anyhow"] }
|
||||
n0-future = "0.3.1"
|
||||
tokio = { version = "1.48.0", features = ["sync"] }
|
||||
tokio-util = "0.7.17"
|
||||
tracing = "0.1.41"
|
||||
url = "2.5.7"
|
||||
web-transport-iroh = { version = "0.1.0", path = "../web-transport-iroh" }
|
||||
446
third_party/iroh-live/iroh-moq/src/lib.rs
vendored
Normal file
446
third_party/iroh-live/iroh-moq/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
use std::{
|
||||
collections::{HashMap, hash_map},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use iroh::{
|
||||
Endpoint, EndpointAddr, EndpointId,
|
||||
endpoint::{Connection, ConnectionError},
|
||||
protocol::ProtocolHandler,
|
||||
};
|
||||
use moq_lite::{BroadcastConsumer, BroadcastProducer, OriginConsumer, OriginProducer};
|
||||
use n0_error::{AnyError, Result, StdResultExt, anyerr, e, stack_error};
|
||||
use n0_future::{
|
||||
FuturesUnordered, StreamExt,
|
||||
boxed::BoxFuture,
|
||||
task::{AbortOnDropHandle, JoinSet},
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{Instrument, debug, error_span, info, instrument};
|
||||
use web_transport_iroh::SessionError;
|
||||
|
||||
pub const ALPN: &[u8] = moq_lite::lite::ALPN.as_bytes();
|
||||
|
||||
#[stack_error(derive, add_meta, from_sources)]
|
||||
#[allow(private_interfaces)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Connect(iroh::endpoint::ConnectError),
|
||||
#[error(transparent)]
|
||||
Moq(#[error(source, std_err)] moq_lite::Error),
|
||||
#[error(transparent)]
|
||||
Server(#[error(source, std_err)] web_transport_iroh::ServerError),
|
||||
#[error("internal consistency error")]
|
||||
InternalConsistencyError(#[error(source)] LiveActorDiedError),
|
||||
#[error("failed to perform request")]
|
||||
Request(#[error(source, std_err)] iroh::endpoint::WriteError),
|
||||
}
|
||||
|
||||
#[stack_error(derive, add_meta, from_sources)]
|
||||
#[allow(private_interfaces)]
|
||||
pub enum SubscribeError {
|
||||
#[error("track was not announced")]
|
||||
NotAnnounced,
|
||||
#[error("track was closed")]
|
||||
Closed,
|
||||
#[error("session was closed")]
|
||||
SessionClosed(#[error(source, std_err)] SessionError),
|
||||
}
|
||||
|
||||
#[stack_error(derive)]
|
||||
#[error("live actor died")]
|
||||
struct LiveActorDiedError;
|
||||
|
||||
impl From<mpsc::error::SendError<ActorMessage>> for LiveActorDiedError {
|
||||
fn from(_value: mpsc::error::SendError<ActorMessage>) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Moq {
|
||||
tx: mpsc::Sender<ActorMessage>,
|
||||
shutdown_token: CancellationToken,
|
||||
_actor_handle: Arc<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
impl Moq {
|
||||
pub fn new(endpoint: Endpoint) -> Self {
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let actor = Actor::new(endpoint);
|
||||
let shutdown_token = actor.shutdown_token.clone();
|
||||
let actor_task = n0_future::task::spawn(async move {
|
||||
actor.run(rx).instrument(error_span!("LiveActor")).await
|
||||
});
|
||||
Self {
|
||||
shutdown_token,
|
||||
tx,
|
||||
_actor_handle: Arc::new(AbortOnDropHandle::new(actor_task)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn protocol_handler(&self) -> MoqProtocolHandler {
|
||||
MoqProtocolHandler {
|
||||
tx: self.tx.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish(&self, name: impl ToString, producer: BroadcastProducer) -> Result<()> {
|
||||
self.tx
|
||||
.send(ActorMessage::PublishBroadcast {
|
||||
broadcast_name: name.to_string(),
|
||||
producer,
|
||||
})
|
||||
.await
|
||||
.std_context("live actor died")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn published_broadcasts(&self) -> Vec<String> {
|
||||
let (reply, reply_rx) = oneshot::channel();
|
||||
if let Err(_) = self.tx.send(ActorMessage::GetPublished { reply }).await {
|
||||
return vec![];
|
||||
}
|
||||
reply_rx.await.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn connect(&self, remote: impl Into<EndpointAddr>) -> Result<MoqSession, AnyError> {
|
||||
// MoqSession::connect(&self.endpoint, addr).await
|
||||
let (reply, reply_rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(ActorMessage::Connect {
|
||||
remote: remote.into(),
|
||||
reply,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| LiveActorDiedError)?;
|
||||
reply_rx
|
||||
.await
|
||||
.map_err(|_| LiveActorDiedError)?
|
||||
.map_err(|err| anyerr!(err))
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
self.shutdown_token.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MoqProtocolHandler {
|
||||
tx: mpsc::Sender<ActorMessage>,
|
||||
}
|
||||
|
||||
impl MoqProtocolHandler {
|
||||
async fn handle_connection(&self, connection: Connection) -> Result<(), Error> {
|
||||
info!(remote = %connection.remote_id().fmt_short(), "accepted");
|
||||
let session = web_transport_iroh::Session::raw(connection);
|
||||
let session = MoqSession::session_accept(session).await?;
|
||||
self.tx
|
||||
.send(ActorMessage::HandleSession { session })
|
||||
.await
|
||||
.map_err(LiveActorDiedError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolHandler for MoqProtocolHandler {
|
||||
async fn accept(&self, connection: Connection) -> Result<(), iroh::protocol::AcceptError> {
|
||||
self.handle_connection(connection)
|
||||
.await
|
||||
.map_err(AnyError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: resubscribing session?
|
||||
// struct MoqSession2 {
|
||||
// session: MoqSession,
|
||||
// tx: mpsc::Sender<ActorMessage>,
|
||||
// remote: EndpointAddr,
|
||||
// }
|
||||
|
||||
// impl MoqSession2 {
|
||||
// pub async fn subscribe(&mut self, name: &str) -> Result<BroadcastConsumer> {
|
||||
// match self.session.subscribe(name).await {
|
||||
// Ok(consumer) => return Ok(consumer),
|
||||
// Err(err) => {
|
||||
// warn!("first attempt to subscribe failed, retrying. reason: {err:#}");
|
||||
// let (reply, reply_rx) = oneshot::channel();
|
||||
// self.tx
|
||||
// .send(ActorMessage::Connect {
|
||||
// remote: self.remote.clone(),
|
||||
// reply,
|
||||
// })
|
||||
// .await
|
||||
// .map_err(|_| LiveActorDiedError)?;
|
||||
// self.session = reply_rx
|
||||
// .await
|
||||
// .map_err(|_| LiveActorDiedError)?
|
||||
// .map_err(|err| anyerr!(err))?;
|
||||
// self.session.subscribe(name).await.map_err(Into::into)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MoqSession {
|
||||
wt_session: web_transport_iroh::Session,
|
||||
publish: OriginProducer,
|
||||
subscribe: OriginConsumer,
|
||||
}
|
||||
|
||||
impl MoqSession {
|
||||
#[instrument(skip_all, fields(remote=tracing::field::Empty))]
|
||||
pub async fn connect(
|
||||
endpoint: &Endpoint,
|
||||
remote_addr: impl Into<EndpointAddr>,
|
||||
) -> Result<Self, Error> {
|
||||
let addr = remote_addr.into();
|
||||
tracing::Span::current().record("remote", tracing::field::display(addr.id.fmt_short()));
|
||||
let connection = endpoint.connect(addr, ALPN).await?;
|
||||
let wt_session = web_transport_iroh::Session::raw(connection);
|
||||
Self::session_connect(wt_session).await
|
||||
}
|
||||
|
||||
pub async fn session_connect(wt_session: web_transport_iroh::Session) -> Result<Self, Error> {
|
||||
let publish = moq_lite::Origin::produce();
|
||||
let subscribe = moq_lite::Origin::produce();
|
||||
// We can drop the moq_lite::Session, it spawns it tasks in the background anyway.
|
||||
// If that changes and it becomes a guard, we should keep it around.
|
||||
let _moq_session =
|
||||
moq_lite::Session::connect(wt_session.clone(), publish.consumer, subscribe.producer)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
publish: publish.producer,
|
||||
subscribe: subscribe.consumer,
|
||||
wt_session,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn session_accept(wt_session: web_transport_iroh::Session) -> Result<Self, Error> {
|
||||
let publish = moq_lite::Origin::produce();
|
||||
let subscribe = moq_lite::Origin::produce();
|
||||
// We can drop the moq_lite::Session, it spawns it tasks in the background anyway.
|
||||
// If that changes and it becomes a guard, we should keep it around.
|
||||
let _moq_session =
|
||||
moq_lite::Session::accept(wt_session.clone(), publish.consumer, subscribe.producer)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
publish: publish.producer,
|
||||
subscribe: subscribe.consumer,
|
||||
wt_session,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote_id(&self) -> EndpointId {
|
||||
self.wt_session.remote_id()
|
||||
}
|
||||
|
||||
pub fn conn(&self) -> &iroh::endpoint::Connection {
|
||||
self.wt_session.conn()
|
||||
}
|
||||
|
||||
pub async fn subscribe(&mut self, name: &str) -> Result<BroadcastConsumer, SubscribeError> {
|
||||
if let Some(reason) = self.conn().close_reason() {
|
||||
return Err(SessionError::from(reason).into());
|
||||
}
|
||||
if let Some(consumer) = self.subscribe.consume_broadcast(name) {
|
||||
return Ok(consumer);
|
||||
}
|
||||
loop {
|
||||
let res = tokio::select! {
|
||||
res = self.subscribe.announced() => res,
|
||||
reason = self.wt_session.closed() => {
|
||||
return Err(reason.into())
|
||||
}
|
||||
};
|
||||
let (path, consumer) = res.ok_or_else(|| e!(SubscribeError::NotAnnounced))?;
|
||||
debug!("peer announced broadcast: {path}");
|
||||
if path.as_str() == name {
|
||||
return consumer.ok_or_else(|| e!(SubscribeError::Closed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn publish(&self, name: String, broadcast: BroadcastConsumer) {
|
||||
self.publish.publish_broadcast(name, broadcast);
|
||||
}
|
||||
|
||||
pub fn close(&self, error_code: u32, reason: &[u8]) {
|
||||
self.wt_session.close(error_code, reason);
|
||||
}
|
||||
|
||||
pub async fn closed(&self) -> web_transport_iroh::SessionError {
|
||||
self.wt_session.closed().await
|
||||
}
|
||||
}
|
||||
|
||||
enum ActorMessage {
|
||||
HandleSession {
|
||||
session: MoqSession,
|
||||
},
|
||||
PublishBroadcast {
|
||||
broadcast_name: BroadcastName,
|
||||
producer: BroadcastProducer,
|
||||
},
|
||||
Connect {
|
||||
remote: EndpointAddr,
|
||||
reply: oneshot::Sender<Result<MoqSession, Arc<AnyError>>>,
|
||||
},
|
||||
GetPublished {
|
||||
reply: oneshot::Sender<Vec<BroadcastName>>,
|
||||
},
|
||||
}
|
||||
|
||||
type BroadcastName = String;
|
||||
|
||||
#[derive()]
|
||||
struct Actor {
|
||||
endpoint: Endpoint,
|
||||
shutdown_token: CancellationToken,
|
||||
publishing: HashMap<BroadcastName, BroadcastProducer>,
|
||||
publishing_closed_futs: FuturesUnordered<BoxFuture<BroadcastName>>,
|
||||
sessions: HashMap<EndpointId, MoqSession>,
|
||||
session_tasks: JoinSet<(EndpointId, Result<(), web_transport_iroh::SessionError>)>,
|
||||
pending_connects: HashMap<EndpointId, Vec<oneshot::Sender<Result<MoqSession, Arc<AnyError>>>>>,
|
||||
pending_connect_tasks: JoinSet<(EndpointId, Result<MoqSession, AnyError>)>,
|
||||
}
|
||||
|
||||
impl Actor {
|
||||
pub fn new(endpoint: Endpoint) -> Self {
|
||||
Self {
|
||||
endpoint,
|
||||
shutdown_token: CancellationToken::new(),
|
||||
publishing: Default::default(),
|
||||
publishing_closed_futs: Default::default(),
|
||||
sessions: Default::default(),
|
||||
session_tasks: Default::default(),
|
||||
pending_connects: Default::default(),
|
||||
pending_connect_tasks: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(mut self, mut inbox: mpsc::Receiver<ActorMessage>) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = inbox.recv() => {
|
||||
match msg {
|
||||
None => break,
|
||||
Some(msg) => self.handle_message(msg)
|
||||
}
|
||||
}
|
||||
Some(res) = self.session_tasks.join_next(), if !self.session_tasks.is_empty() => {
|
||||
let (endpoint_id, res) = res.expect("session task panicked");
|
||||
info!(remote=%endpoint_id.fmt_short(), "session closed: {res:?}");
|
||||
self.sessions.remove(&endpoint_id);
|
||||
}
|
||||
Some(name) = self.publishing_closed_futs.next(), if !self.publishing_closed_futs.is_empty() => {
|
||||
self.publishing.remove(&name);
|
||||
}
|
||||
Some(res) = self.pending_connect_tasks.join_next(), if !self.pending_connect_tasks.is_empty() => {
|
||||
let (endpoint_id, res) = res.expect("connect task panicked");
|
||||
match res {
|
||||
Ok(session) => {
|
||||
info!(remote=%endpoint_id.fmt_short(), "connected");
|
||||
self.handle_incoming_session(session);
|
||||
}
|
||||
Err(err) => {
|
||||
info!(remote=%endpoint_id.fmt_short(), "connect failed: {err:#}");
|
||||
let replies = self.pending_connects.remove(&endpoint_id).into_iter().flatten();
|
||||
let err = Arc::new(err);
|
||||
for reply in replies {
|
||||
reply.send(Err(err.clone())).ok();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, msg: ActorMessage) {
|
||||
match msg {
|
||||
ActorMessage::HandleSession { session: msg } => self.handle_incoming_session(msg),
|
||||
ActorMessage::PublishBroadcast {
|
||||
broadcast_name: name,
|
||||
producer,
|
||||
} => self.handle_publish_broadcast(name, producer),
|
||||
ActorMessage::Connect { remote, reply } => self.handle_connect(remote, reply),
|
||||
ActorMessage::GetPublished { reply } => {
|
||||
let names = self.publishing.keys().cloned().collect();
|
||||
reply.send(names).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_incoming_session(&mut self, session: MoqSession) {
|
||||
tracing::info!("handle new incoming session");
|
||||
let remote = session.remote_id();
|
||||
for (name, producer) in self.publishing.iter() {
|
||||
session.publish(name.to_string(), producer.consume());
|
||||
}
|
||||
self.sessions.insert(remote, session.clone());
|
||||
for reply in self.pending_connects.remove(&remote).into_iter().flatten() {
|
||||
reply.send(Ok(session.clone())).ok();
|
||||
}
|
||||
|
||||
let shutdown = self.shutdown_token.child_token();
|
||||
self.session_tasks.spawn(async move {
|
||||
let res = tokio::select! {
|
||||
_ = shutdown.cancelled() => {
|
||||
session.close(0u32.into(), b"cancelled");
|
||||
Ok(())
|
||||
}
|
||||
result = session.closed() => match result {
|
||||
SessionError::ConnectionError(ConnectionError::LocallyClosed) => Ok(()),
|
||||
err @ _ => Err(err)
|
||||
},
|
||||
};
|
||||
(remote, res)
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_publish_broadcast(&mut self, name: BroadcastName, producer: BroadcastProducer) {
|
||||
for session in self.sessions.values_mut() {
|
||||
session
|
||||
.publish
|
||||
.publish_broadcast(name.clone(), producer.consume());
|
||||
}
|
||||
let closed = producer.consume().closed();
|
||||
self.publishing.insert(name.clone(), producer);
|
||||
self.publishing_closed_futs.push(Box::pin(async move {
|
||||
closed.await;
|
||||
name
|
||||
}));
|
||||
}
|
||||
|
||||
fn handle_connect(
|
||||
&mut self,
|
||||
remote: EndpointAddr,
|
||||
reply: oneshot::Sender<Result<MoqSession, Arc<AnyError>>>,
|
||||
) {
|
||||
let remote_id = remote.id;
|
||||
if let Some(session) = self.sessions.get(&remote_id) {
|
||||
reply.send(Ok(session.clone())).ok();
|
||||
return;
|
||||
}
|
||||
match self.pending_connects.entry(remote_id) {
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().push(reply);
|
||||
}
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let endpoint = self.endpoint.clone();
|
||||
self.pending_connect_tasks.spawn(async move {
|
||||
let res = MoqSession::connect(&endpoint, remote)
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
(remote_id, res)
|
||||
});
|
||||
entry.insert(Default::default()).push(reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue