use std::sync::Arc;
use anyhow::Context;
use tokio::sync::oneshot;
use url::Url;
use crate::{Error, Id, NonZeroSlab, State, ffi};
struct TaskEntry {
close: Option<oneshot::Sender<()>>,
callback: ffi::OnStatus,
stats: moq_tokio::connection::Monitor,
bandwidth: moq_net::bandwidth::Allocator,
}
pub(crate) struct Connect {
pub config: crate::client::Config,
pub url: Url,
pub publish: Option<moq_net::origin::Producer>,
pub consume: Option<moq_net::origin::Producer>,
pub callback: ffi::OnStatus,
}
impl Connect {
pub fn prepare(self) -> Result<PreparedConnect, Error> {
let mut client = self
.config
.connect
.clone()
.init(self.config.quic.clone())
.map_err(|err| Error::InvalidConfig(err.to_string()))?;
if let Some(publish) = &self.publish {
client = client.with_publisher(publish);
}
if let Some(consume) = &self.consume {
client = client.with_subscriber(consume.clone());
}
Ok(PreparedConnect {
client,
url: self.url,
publish: self.publish,
consume: self.consume,
callback: self.callback,
})
}
}
pub(crate) struct PreparedConnect {
client: moq_tokio::Client,
url: Url,
publish: Option<moq_net::origin::Producer>,
consume: Option<moq_net::origin::Producer>,
callback: ffi::OnStatus,
}
#[derive(Default)]
pub struct Session {
task: NonZeroSlab<Option<TaskEntry>>,
}
impl Session {
pub fn connect(&mut self, request: PreparedConnect) -> Result<Id, Error> {
let PreparedConnect {
client,
url,
publish,
consume,
callback,
} = request;
let reconnect = client.connect(url);
let stats = reconnect.monitor();
let bandwidth = moq_net::bandwidth::Allocator::new(reconnect.send_bandwidth());
let closed = oneshot::channel();
let entry = TaskEntry {
close: Some(closed.0),
callback,
stats,
bandwidth,
};
let id = self.task.insert(Some(entry))?;
tokio::spawn(async move {
let _publish = publish;
let _consume = consume;
let res = tokio::select! {
_ = closed.1 => Ok(()),
res = Self::report(callback, reconnect) => res,
};
let entry = State::lock().session.task.remove(id).flatten();
if let Some(entry) = entry {
entry.callback.call(res);
}
});
Ok(id)
}
pub fn bandwidth(&self, id: Id) -> Result<moq_net::bandwidth::Allocator, Error> {
Ok(self
.task
.get(id)
.and_then(|entry| entry.as_ref())
.ok_or(Error::SessionNotFound)?
.bandwidth
.clone())
}
pub fn stats(&self, id: Id) -> Result<moq_net::session::Stats, Error> {
self.task
.get(id)
.and_then(|entry| entry.as_ref())
.ok_or(Error::SessionNotFound)?
.stats
.stats()
.ok_or(Error::Offline)
}
pub fn snapshot(&self, id: Id) -> Result<moq_tokio::connection::Snapshot, Error> {
self.task
.get(id)
.and_then(|entry| entry.as_ref())
.ok_or(Error::SessionNotFound)?
.stats
.snapshot()
.ok_or(Error::Offline)
}
async fn report(callback: ffi::OnStatus, mut reconnect: moq_tokio::Connection) -> Result<(), Error> {
let mut connects: u64 = 0;
loop {
if let moq_tokio::Status::Connected = reconnect.status().await.map_err(map_connect_error)? {
connects += 1;
let code = i32::try_from(connects)
.context("connection epoch exceeded i32::MAX")
.map_err(|err| Error::Connect(Arc::new(err)))?;
callback.call(code);
}
}
}
pub fn close(&mut self, id: Id) -> Result<(), Error> {
self.task
.get_mut(id)
.and_then(|entry| entry.as_mut())
.ok_or(Error::SessionNotFound)?
.close
.take()
.ok_or(Error::SessionNotFound)?;
Ok(())
}
}
fn map_connect_error(err: moq_tokio::Error) -> Error {
match err {
moq_tokio::Error::MoqNet(moq_net::Error::Unauthorized) => Error::Unauthorized,
moq_tokio::Error::MoqNet(err) => err.into(),
err => match err.connect_error() {
Some(moq_tokio::ConnectError::Unauthorized) => Error::Unauthorized,
Some(moq_tokio::ConnectError::Forbidden) => Error::Forbidden,
_ => Error::Connect(Arc::new(err.into())),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::ReturnCode;
#[test]
fn maps_native_auth_connect_errors() {
assert!(matches!(
map_connect_error(moq_tokio::ConnectError::Unauthorized.into()),
Error::Unauthorized
));
assert!(matches!(
map_connect_error(moq_tokio::ConnectError::Forbidden.into()),
Error::Forbidden
));
assert!(matches!(
map_connect_error(moq_net::Error::Unauthorized.into()),
Error::Unauthorized
));
assert!(matches!(
map_connect_error(moq_net::Error::from(moq_net::SessionError::Unauthorized).into()),
Error::Moq(moq_net::Error::Session(moq_net::SessionError::Unauthorized))
));
assert!(matches!(
map_connect_error(moq_tokio::Error::ConnectFailed),
Error::Connect(_)
));
assert_eq!(Error::Unauthorized.code(), -34);
assert_eq!(Error::Forbidden.code(), -35);
assert_eq!(map_connect_error(moq_net::Error::Unauthorized.into()).code(), -34);
assert_eq!(
map_connect_error(moq_net::Error::from(moq_net::SessionError::Unauthorized).into()).code(),
-2
);
assert_eq!(map_connect_error(moq_tokio::Error::ConnectFailed).code(), -5);
}
}