publish client-sdk-native (#12)

* Create README.md

* add example

* crates & examples

* fix syntax

* add LICENSE

* thirdparty LICENSE

* rearrange repo

* fix build

* egui versions

* prepare publish

* fix demo compilation

* add test ci

* Update rust.yml

* forgot runs-on

* install protoc before building

* avoid rate limit

* include submodules

* updates to readme

* cache rust builds

Co-authored-by: David Zhao <[email protected]>
Co-authored-by: David Zhao <[email protected]>
This commit is contained in:
Théo Monnom
2023-01-02 20:13:48 +01:00
committed by GitHub
co-authored by David Zhao David Zhao
parent a927baac94
commit a07b3451a3
102 changed files with 893 additions and 344 deletions
+37
View File
@@ -0,0 +1,37 @@
use std::fmt::{Debug, Formatter};
use tracing::trace;
use livekit_webrtc::peer_connection_factory::PeerConnectionFactory;
use livekit_webrtc::webrtc::RTCRuntime;
/// SAFETY: The order of initialization and deletion is important for LKRuntime.
/// See the C++ constructors & destructors of these fields
pub struct LKRuntime {
pub pc_factory: PeerConnectionFactory,
pub rtc_runtime: RTCRuntime,
}
impl Debug for LKRuntime {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "LKRuntime")
}
}
impl Default for LKRuntime {
fn default() -> Self {
trace!("LKRuntime::default()");
let rtc_runtime = RTCRuntime::new();
Self {
pc_factory: PeerConnectionFactory::new(rtc_runtime.clone()),
rtc_runtime,
}
}
}
impl Drop for LKRuntime {
fn drop(&mut self) {
trace!("LKRuntime::drop()");
}
}
+471
View File
@@ -0,0 +1,471 @@
use futures::future::BoxFuture;
use futures::FutureExt;
use livekit_webrtc::data_channel::DataSendError;
use livekit_webrtc::jsep::SdpParseError;
use livekit_webrtc::media_stream::{MediaStream, MediaStreamTrackHandle};
use livekit_webrtc::rtc_error::RTCError;
use livekit_webrtc::rtp_receiver::RtpReceiver;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use std::time::Duration;
use thiserror::Error;
use tokio::sync::RwLock as AsyncRwLock;
use tokio::task::JoinHandle;
use tokio::time::{interval, Interval};
use lazy_static::lazy_static;
use tokio::sync::{mpsc, oneshot};
use tracing::{error, info, warn};
use crate::proto::{
self as proto, data_packet, DataPacket, JoinResponse, ParticipantUpdate, SpeakerInfo,
};
use crate::rtc_engine::lk_runtime::LKRuntime;
use crate::signal_client::{SignalError, SignalOptions};
use self::rtc_session::{RTCSession, SessionEvent, SessionEvents, SessionInfo};
mod lk_runtime;
mod pc_transport;
mod rtc_events;
mod rtc_session;
pub(crate) type EngineEmitter = mpsc::Sender<EngineEvent>;
pub(crate) type EngineEvents = mpsc::Receiver<EngineEvent>;
pub(crate) type EngineResult<T> = Result<T, EngineError>;
#[derive(Debug, Clone, Eq, PartialEq)]
#[repr(u8)]
pub enum SimulateScenario {
SignalReconnect,
Speaker,
NodeFailure,
ServerLeave,
Migration,
ForceTcp,
ForceTls,
}
#[derive(Error, Debug)]
pub enum EngineError {
#[error("signal failure: {0}")]
Signal(#[from] SignalError),
#[error("internal webrtc failure")]
Rtc(#[from] RTCError),
#[error("failed to parse sdp")]
Parse(#[from] SdpParseError),
#[error("serde error")]
Serde(#[from] serde_json::Error),
#[error("failed to send data to the datachannel")]
Data(#[from] DataSendError),
#[error("connection error: {0}")]
Connection(String),
#[error("decode error")]
Decode(#[from] prost::DecodeError),
#[error("internal error: {0}")]
Internal(String), // Unexpected error
}
#[derive(Debug)]
pub enum EngineEvent {
ParticipantUpdate(ParticipantUpdate),
MediaTrack {
track: MediaStreamTrackHandle,
stream: MediaStream,
receiver: RtpReceiver,
},
Data {
participant_sid: String,
payload: Vec<u8>,
kind: data_packet::Kind,
},
SpeakersChanged {
speakers: Vec<SpeakerInfo>,
},
ConnectionQuality {
updates: Vec<proto::ConnectionQualityInfo>,
},
Resuming,
Resumed,
Restarting,
Restarted,
Disconnected,
}
pub const RECONNECT_ATTEMPTS: u32 = 10;
pub const RECONNECT_INTERVAL: Duration = Duration::from_secs(5);
lazy_static! {
// Share one LKRuntime across all RTCEngine instances
static ref LK_RUNTIME: Mutex<Weak<LKRuntime>> = Mutex::new(Weak::new());
}
///
/// Represents a running RTCSession with the ability to close the session
/// and the engine_task
#[derive(Debug)]
struct EngineHandle {
session: RTCSession,
engine_task: JoinHandle<()>,
close_sender: oneshot::Sender<()>,
}
#[derive(Debug)]
struct EngineInner {
lk_runtime: Arc<LKRuntime>,
session_info: Mutex<Option<SessionInfo>>, // Last/Current Sessioninfo
running_handle: AsyncRwLock<Option<EngineHandle>>,
opened: AtomicBool,
engine_emitter: EngineEmitter,
// Reconnecting fields
reconnecting: AtomicBool,
full_reconnect: AtomicBool,
reconnect_interval: Mutex<Interval>,
}
#[derive(Debug)]
pub struct RTCEngine {
inner: Arc<EngineInner>,
}
impl RTCEngine {
pub fn new() -> (Self, EngineEvents) {
let mut lk_runtime = None;
{
let mut lk_runtime_ref = LK_RUNTIME.lock();
lk_runtime = lk_runtime_ref.upgrade();
if lk_runtime.is_none() {
let new_runtime = Arc::new(LKRuntime::default());
*lk_runtime_ref = Arc::downgrade(&new_runtime);
lk_runtime = Some(new_runtime);
}
}
let (engine_emitter, engine_events) = mpsc::channel(8);
let inner = Arc::new(EngineInner {
lk_runtime: lk_runtime.unwrap(),
session_info: Default::default(),
running_handle: Default::default(),
opened: Default::default(),
engine_emitter,
reconnecting: Default::default(),
full_reconnect: Default::default(),
reconnect_interval: Mutex::new(interval(RECONNECT_INTERVAL)),
});
(Self { inner }, engine_events)
}
#[tracing::instrument]
pub async fn connect(
&self,
url: &str,
token: &str,
options: SignalOptions,
) -> EngineResult<()> {
self.inner.connect(url, token, options).await
}
#[tracing::instrument]
pub async fn close(&self) {
self.inner.close().await
}
#[tracing::instrument(skip(data))]
pub async fn publish_data(
&self,
data: &DataPacket,
kind: data_packet::Kind,
) -> EngineResult<()> {
self.inner.wait_reconnection().await?;
self.inner
.running_handle
.read()
.await
.as_ref()
.unwrap()
.session
.publish_data(data, kind)
.await
}
pub async fn simulate_scenario(&self, scenario: SimulateScenario) -> EngineResult<()> {
self.inner.wait_reconnection().await?;
self.inner
.running_handle
.read()
.await
.as_ref()
.unwrap()
.session
.simulate_scenario(scenario)
.await;
Ok(())
}
pub fn join_response(&self) -> Option<JoinResponse> {
if let Some(info) = self.inner.session_info.lock().as_ref() {
Some(info.join_response.clone())
} else {
None
}
}
}
impl EngineInner {
async fn engine_task(
self: Arc<Self>,
mut session_events: SessionEvents,
mut close_receiver: oneshot::Receiver<()>,
) {
loop {
tokio::select! {
res = session_events.recv() => {
if let Some(event) = res {
if let Err(err) = self.on_session_event(event).await {
error!("failed to handle session event: {:?}", err);
}
} else {
panic!("rtc_sessions has been closed unexpectedly");
}
},
_ = &mut close_receiver => {
break;
}
}
}
}
async fn on_session_event(self: &Arc<Self>, event: SessionEvent) -> EngineResult<()> {
match event {
SessionEvent::Close {
source,
reason,
can_reconnect,
retry_now,
full_reconnect,
} => {
info!("received session close: {}, {:?}", source, reason);
if can_reconnect {
self.clone().try_reconnect(retry_now, full_reconnect);
} else {
self.close().await;
}
}
SessionEvent::Data {
participant_sid,
payload,
kind,
} => {
let _ = self
.engine_emitter
.send(EngineEvent::Data {
participant_sid,
payload,
kind,
})
.await;
}
SessionEvent::MediaTrack {
track,
stream,
receiver,
} => {
let _ = self
.engine_emitter
.send(EngineEvent::MediaTrack {
track,
stream,
receiver,
})
.await;
}
SessionEvent::SpeakersChanged { speakers } => {
let _ = self
.engine_emitter
.send(EngineEvent::SpeakersChanged { speakers })
.await;
}
SessionEvent::ConnectionQuality { updates } => {
let _ = self
.engine_emitter
.send(EngineEvent::ConnectionQuality { updates })
.await;
}
SessionEvent::Connected => {}
}
Ok(())
}
fn connect<'a>(
self: &'a Arc<Self>,
url: &'a str,
token: &'a str,
options: SignalOptions,
) -> BoxFuture<'a, EngineResult<()>> {
async {
let (session_emitter, session_events) = mpsc::unbounded_channel();
let session = RTCSession::connect(
url,
token,
options,
self.lk_runtime.clone(),
session_emitter,
)
.await?;
let (close_sender, close_receiver) = oneshot::channel();
let engine_task =
tokio::spawn(self.clone().engine_task(session_events, close_receiver));
*self.session_info.lock() = Some(session.info().clone());
*self.running_handle.write().await = Some(EngineHandle {
session,
engine_task,
close_sender,
});
self.opened.store(true, Ordering::SeqCst);
Ok(())
}
.boxed()
}
async fn terminate_session(&self) {
if let Some(handle) = self.running_handle.write().await.take() {
handle.session.close().await;
let _ = handle.close_sender.send(());
let _ = handle.engine_task.await;
}
}
async fn close(&self) {
self.opened.store(false, Ordering::SeqCst);
self.terminate_session().await;
let _ = self.engine_emitter.send(EngineEvent::Disconnected).await;
}
// Wait for the reconnection task to finish
// Return directly if no open RTCSession
async fn wait_reconnection(&self) -> EngineResult<()> {
if !self.opened.load(Ordering::SeqCst) {
Err(EngineError::Connection("not opened".to_owned()))?
}
while self.reconnecting.load(Ordering::Acquire) {
tokio::task::yield_now().await;
}
if self.running_handle.read().await.is_none() {
Err(EngineError::Connection("reconnection failed".to_owned()))?
}
Ok(())
}
/// Start the reconnect task if not already started
fn try_reconnect(self: Arc<Self>, retry_now: bool, full_reconnect: bool) {
if !self.opened.load(Ordering::SeqCst) {
return;
}
if self.reconnecting.load(Ordering::SeqCst) {
if retry_now {
self.reconnect_interval.lock().reset();
self.full_reconnect.store(full_reconnect, Ordering::SeqCst);
}
return;
}
warn!("reconnecting RTCEngine...");
self.reconnecting.store(true, Ordering::SeqCst);
self.full_reconnect.store(full_reconnect, Ordering::SeqCst);
self.reconnect_interval.lock().reset();
tokio::spawn({
let inner = self.clone();
async move {
let res = inner.reconnect_task().await;
inner.reconnecting.store(false, Ordering::SeqCst);
if res.is_ok() {
warn!("RTCEngine successfully reconnected")
} else {
error!("failed to reconnect after {} attemps", RECONNECT_ATTEMPTS);
inner.close().await;
}
}
});
}
/// Called every time the PeerConnection or the SignalClient is closed
/// We first try to resume the connection, if it fails, we start a full reconnect.
async fn reconnect_task(self: &Arc<Self>) -> EngineResult<()> {
for i in 0..RECONNECT_ATTEMPTS {
if !self.opened.load(Ordering::Acquire) {
// The user closed the RTCEngine, cancel the reconnection task
return Ok(());
}
if self.full_reconnect.load(Ordering::SeqCst) {
if i == 0 {
let _ = self.engine_emitter.send(EngineEvent::Restarting).await;
}
info!("restarting connection... attempt: {}", i);
if let Err(err) = self.try_restart_connection().await {
error!("restarting connection failed: {}", err);
} else {
let _ = self.engine_emitter.send(EngineEvent::Restarted).await;
return Ok(());
}
} else {
if i == 0 {
let _ = self.engine_emitter.send(EngineEvent::Resuming).await;
}
info!("resuming connection... attempt: {}", i);
if let Err(err) = self.try_resume_connection().await {
error!("resuming connection failed: {}", err);
if let EngineError::Signal(_) = err {
self.full_reconnect.store(true, Ordering::SeqCst);
}
} else {
let _ = self.engine_emitter.send(EngineEvent::Resumed).await;
return Ok(());
}
}
self.reconnect_interval.lock().tick().await;
}
Err(EngineError::Connection("failed to reconnect".to_owned()))
}
/// Try to recover the connection by doing a full reconnect.
/// It recreates a new RTCSession
async fn try_restart_connection(self: &Arc<Self>) -> EngineResult<()> {
let info = self.session_info.lock().clone().unwrap();
self.terminate_session().await;
self.connect(&info.url, &info.token, info.options).await?;
self.running_handle
.read()
.await
.as_ref()
.unwrap()
.session
.wait_pc_connection()
.await
// TODO(theomonnom): Resend SignalClient queue
}
/// Try to restart the current session
async fn try_resume_connection(&self) -> EngineResult<()> {
let handle = self.running_handle.read().await;
handle.as_ref().unwrap().session.restart().await?;
handle.as_ref().unwrap().session.wait_pc_connection().await
}
}
+178
View File
@@ -0,0 +1,178 @@
use std::fmt::{Debug, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use tracing::{event, Level};
use livekit_webrtc::jsep::{IceCandidate, SessionDescription};
use livekit_webrtc::peer_connection::{
IceConnectionState, PeerConnection, RTCOfferAnswerOptions, SignalingState,
};
use livekit_webrtc::rtc_error::RTCError;
use crate::proto::SignalTarget;
const NEGOTIATION_FREQUENCY: Duration = Duration::from_millis(150);
pub type OnOfferHandler = Box<
dyn (FnMut(SessionDescription) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>>)
+ Send
+ Sync,
>;
pub struct PCTransport {
signal_target: SignalTarget,
peer_connection: PeerConnection,
pending_candidates: Vec<IceCandidate>,
on_offer_handler: Option<OnOfferHandler>,
renegotiate: bool,
restarting_ice: bool,
}
impl Debug for PCTransport {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.write_str("PCTransport")
}
}
impl PCTransport {
pub fn new(peer_connection: PeerConnection, signal_target: SignalTarget) -> Self {
Self {
signal_target,
peer_connection,
pending_candidates: Vec::default(),
on_offer_handler: None,
restarting_ice: false,
renegotiate: false,
}
}
pub fn is_connected(&self) -> bool {
self.peer_connection.ice_connection_state() == IceConnectionState::IceConnectionConnected
|| self.peer_connection.ice_connection_state()
== IceConnectionState::IceConnectionCompleted
}
pub fn peer_connection(&mut self) -> &mut PeerConnection {
&mut self.peer_connection
}
pub fn signal_target(&self) -> SignalTarget {
self.signal_target.clone()
}
pub fn on_offer(&mut self, handler: OnOfferHandler) {
self.on_offer_handler = Some(handler);
}
pub fn prepare_ice_restart(&mut self) {
self.restarting_ice = true;
}
pub fn close(&mut self) {
self.peer_connection.close();
}
#[tracing::instrument(level = Level::DEBUG)]
pub async fn add_ice_candidate(&mut self, ice_candidate: IceCandidate) -> Result<(), RTCError> {
if self.peer_connection.remote_description().is_some() && !self.restarting_ice {
self.peer_connection
.add_ice_candidate(ice_candidate)
.await?;
return Ok(());
}
self.pending_candidates.push(ice_candidate);
Ok(())
}
#[tracing::instrument(level = Level::DEBUG)]
pub async fn set_remote_description(
&mut self,
remote_description: SessionDescription,
) -> Result<(), RTCError> {
self.peer_connection
.set_remote_description(remote_description)
.await?;
for ic in self.pending_candidates.drain(..) {
self.peer_connection.add_ice_candidate(ic).await?;
}
self.restarting_ice = false;
if self.renegotiate {
self.renegotiate = false;
self.create_and_send_offer(RTCOfferAnswerOptions::default())
.await?;
}
Ok(())
}
#[tracing::instrument(level = Level::DEBUG)]
pub async fn negotiate(&mut self) -> Result<(), RTCError> {
// TODO(theomonnom) Debounce here with NEGOTIATION_FREQUENCY
self.create_and_send_offer(RTCOfferAnswerOptions::default())
.await
}
#[tracing::instrument(level = Level::DEBUG)]
pub async fn create_anwser(
&mut self,
offer: SessionDescription,
options: RTCOfferAnswerOptions,
) -> Result<SessionDescription, RTCError> {
self.set_remote_description(offer).await?;
let answer = self
.peer_connection()
.create_answer(RTCOfferAnswerOptions::default())
.await?;
self.peer_connection()
.set_local_description(answer.clone())
.await?;
Ok(answer)
}
#[tracing::instrument(level = Level::DEBUG)]
pub async fn create_and_send_offer(
&mut self,
options: RTCOfferAnswerOptions,
) -> Result<(), RTCError> {
if self.on_offer_handler.is_none() {
return Ok(());
}
if options.ice_restart {
event!(Level::TRACE, "restarting ICE");
self.restarting_ice = true;
}
if self.peer_connection.signaling_state() == SignalingState::HaveLocalOffer {
if options.ice_restart {
if let Some(remote_description) = self.peer_connection.remote_description() {
self.peer_connection
.set_remote_description(remote_description)
.await?;
} else {
event!(
Level::ERROR,
"trying to restart ICE when the pc doesn't have remote description"
);
}
} else {
self.renegotiate = true;
return Ok(());
}
}
let offer = self.peer_connection.create_offer(options).await?;
self.peer_connection
.set_local_description(offer.clone())
.await?;
self.on_offer_handler.as_mut().unwrap()(offer).await;
Ok(())
}
}
+145
View File
@@ -0,0 +1,145 @@
use livekit_webrtc::data_channel::{DataChannel, OnMessageHandler};
use livekit_webrtc::jsep::{IceCandidate, SessionDescription};
use livekit_webrtc::media_stream::MediaStream;
use livekit_webrtc::peer_connection::{
OnAddTrackHandler, OnConnectionChangeHandler, OnDataChannelHandler, OnIceCandidateErrorHandler,
OnIceCandidateHandler, PeerConnectionState,
};
use livekit_webrtc::rtp_receiver::RtpReceiver;
use tokio::sync::mpsc;
use tracing::error;
use crate::proto::SignalTarget;
use crate::rtc_engine::pc_transport::OnOfferHandler;
use super::pc_transport::PCTransport;
pub type RTCEmitter = mpsc::UnboundedSender<RTCEvent>;
pub type RTCEvents = mpsc::UnboundedReceiver<RTCEvent>;
#[derive(Debug)]
pub enum RTCEvent {
IceCandidate {
ice_candidate: IceCandidate,
target: SignalTarget,
},
ConnectionChange {
state: PeerConnectionState,
target: SignalTarget,
},
DataChannel {
data_channel: DataChannel,
target: SignalTarget,
},
// TODO (theomonnom): Move Offer to PCTransport
Offer {
offer: SessionDescription,
target: SignalTarget,
},
AddTrack {
rtp_receiver: RtpReceiver,
streams: Vec<MediaStream>,
target: SignalTarget,
},
Data {
data: Vec<u8>,
binary: bool,
},
}
/// Handlers used to forward events to a channel
/// Every callback here is called on the signaling thread
fn on_connection_change(target: SignalTarget, emitter: RTCEmitter) -> OnConnectionChangeHandler {
Box::new(move |state| {
let _ = emitter.send(RTCEvent::ConnectionChange { state, target });
})
}
fn on_ice_candidate(target: SignalTarget, emitter: RTCEmitter) -> OnIceCandidateHandler {
Box::new(move |ice_candidate| {
let _ = emitter.send(RTCEvent::IceCandidate {
ice_candidate,
target,
});
})
}
fn on_offer(target: SignalTarget, emitter: RTCEmitter) -> OnOfferHandler {
Box::new(move |offer| {
let _ = emitter.send(RTCEvent::Offer { offer, target });
Box::pin(async {})
})
}
fn on_data_channel(target: SignalTarget, emitter: RTCEmitter) -> OnDataChannelHandler {
Box::new(move |mut data_channel| {
data_channel.on_message(on_message(emitter.clone()));
let _ = emitter.send(RTCEvent::DataChannel {
data_channel,
target,
});
})
}
fn on_add_track(target: SignalTarget, emitter: RTCEmitter) -> OnAddTrackHandler {
Box::new(move |rtp_receiver, streams| {
let _ = emitter.send(RTCEvent::AddTrack {
rtp_receiver,
streams,
target,
});
})
}
fn on_ice_candidate_error(
target: SignalTarget,
_emitter: RTCEmitter,
) -> OnIceCandidateErrorHandler {
Box::new(move |address, port, url, error_code, error_text| {
error!(
"ICE candidate error ({:?}): address: {} - port: {} - url: {} - error_code: {} - error_text: {}",
target, address, port, url, error_code, error_text
);
})
}
pub fn forward_pc_events(transport: &mut PCTransport, rtc_emitter: RTCEmitter) {
let signal_target = transport.signal_target();
transport
.peer_connection()
.on_ice_candidate(on_ice_candidate(signal_target, rtc_emitter.clone()));
transport
.peer_connection()
.on_data_channel(on_data_channel(signal_target, rtc_emitter.clone()));
transport
.peer_connection()
.on_add_track(on_add_track(signal_target, rtc_emitter.clone()));
transport
.peer_connection()
.on_connection_change(on_connection_change(signal_target, rtc_emitter.clone()));
transport
.peer_connection()
.on_ice_candidate_error(on_ice_candidate_error(signal_target, rtc_emitter.clone()));
transport.on_offer(on_offer(transport.signal_target(), rtc_emitter.clone()));
}
fn on_message(emitter: RTCEmitter) -> OnMessageHandler {
Box::new(move |data, binary| {
let _ = emitter.send(RTCEvent::Data {
data: data.to_vec(),
binary,
});
})
}
pub fn forward_dc_events(dc: &mut DataChannel, rtc_emitter: RTCEmitter) {
dc.on_message(on_message(rtc_emitter.clone()));
}
+742
View File
@@ -0,0 +1,742 @@
use livekit_webrtc::media_stream::{MediaStream, MediaStreamTrackHandle};
use livekit_webrtc::rtp_receiver::RtpReceiver;
use parking_lot::Mutex;
use std::convert::TryInto;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::task::JoinHandle;
use tokio::sync::{mpsc, watch, Mutex as AsyncMutex};
use tokio::time::sleep;
use prost::Message;
use serde::{Deserialize, Serialize};
use tracing::{debug, error, trace, warn};
use crate::{proto, signal_client};
use livekit_webrtc::data_channel::{DataChannel, DataChannelInit, DataState};
use livekit_webrtc::jsep::{IceCandidate, SessionDescription};
use livekit_webrtc::peer_connection::{
IceConnectionState, PeerConnectionState, RTCOfferAnswerOptions,
};
use livekit_webrtc::peer_connection_factory::RTCConfiguration;
use crate::proto::data_packet::Value;
use crate::proto::{
data_packet, signal_request, signal_response, CandidateProtocol, DataPacket, DisconnectReason,
JoinResponse, SignalTarget, TrickleRequest,
};
use crate::rtc_engine::lk_runtime::LKRuntime;
use crate::rtc_engine::pc_transport::PCTransport;
use crate::rtc_engine::rtc_events::{RTCEvent, RTCEvents};
use crate::signal_client::{SignalClient, SignalEvent, SignalEvents, SignalOptions};
use super::{rtc_events, EngineError, EngineResult, SimulateScenario};
pub const MAX_ICE_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
pub const LOSSY_DC_LABEL: &str = "_lossy";
pub const RELIABLE_DC_LABEL: &str = "_reliable";
pub type SessionEmitter = mpsc::UnboundedSender<SessionEvent>;
pub type SessionEvents = mpsc::UnboundedReceiver<SessionEvent>;
#[derive(Debug)]
pub enum SessionEvent {
Data {
participant_sid: String,
payload: Vec<u8>,
kind: proto::data_packet::Kind,
},
MediaTrack {
track: MediaStreamTrackHandle,
stream: MediaStream,
receiver: RtpReceiver,
},
SpeakersChanged {
speakers: Vec<proto::SpeakerInfo>,
},
ConnectionQuality {
updates: Vec<proto::ConnectionQualityInfo>,
},
// TODO(theomonnom): Move entirely the reconnection logic on mod.rs
Close {
source: String,
reason: DisconnectReason,
can_reconnect: bool,
full_reconnect: bool,
retry_now: bool,
},
Connected,
}
#[repr(u8)]
pub enum PCState {
New,
Connected,
Disconnected,
Reconnecting,
Closed,
}
impl TryInto<PCState> for u8 {
type Error = &'static str;
fn try_into(self) -> Result<PCState, Self::Error> {
match self {
0 => Ok(PCState::New),
1 => Ok(PCState::Connected),
2 => Ok(PCState::Disconnected),
3 => Ok(PCState::Reconnecting),
4 => Ok(PCState::Closed),
_ => Err("invalid PCState"),
}
}
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
struct IceCandidateJSON {
sdpMid: String,
sdpMLineIndex: i32,
candidate: String,
}
#[derive(Debug, Clone, Default)]
pub struct SessionInfo {
pub url: String,
pub token: String,
pub options: SignalOptions,
pub join_response: JoinResponse,
}
/// Fields shared with rtc_task and signal_task
#[derive(Debug)]
struct SessionInner {
info: SessionInfo,
signal_client: Arc<SignalClient>,
pc_state: AtomicU8, // PCState
has_published: AtomicBool,
publisher_pc: AsyncMutex<PCTransport>,
subscriber_pc: AsyncMutex<PCTransport>,
// Publisher data channels
// used to send data to other participants ( The SFU forwards the messages )
lossy_dc: DataChannel,
reliable_dc: DataChannel,
// Keep a strong reference to the subscriber datachannels,
// so we can receive data from other participants
subscriber_dc: Mutex<Vec<DataChannel>>,
emitter: SessionEmitter,
}
/// This struct holds a WebRTC session
/// The session changes at every reconnection
///
/// RTCSession is also responsable for the signaling and the negotation
#[derive(Debug)]
pub struct RTCSession {
lk_runtime: Arc<LKRuntime>,
inner: Arc<SessionInner>,
close_emitter: watch::Sender<bool>, // false = is_running
signal_task: JoinHandle<()>,
rtc_task: JoinHandle<()>,
}
impl RTCSession {
pub async fn connect(
url: &str,
token: &str,
options: SignalOptions,
lk_runtime: Arc<LKRuntime>,
session_emitter: SessionEmitter,
) -> EngineResult<Self> {
// Connect to the SignalClient
let (signal_client, mut signal_events) = SignalClient::new();
let signal_client = Arc::new(signal_client);
signal_client.connect(url, token, options.clone()).await?;
let join_response = signal_client::utils::next_join_response(&mut signal_events).await?;
debug!("received JoinResponse: {:?}", join_response);
let (rtc_emitter, rtc_events) = mpsc::unbounded_channel();
let rtc_config = RTCConfiguration::from(join_response.clone());
let mut publisher_pc = PCTransport::new(
lk_runtime
.pc_factory
.create_peer_connection(rtc_config.clone())?,
SignalTarget::Publisher,
);
let mut subscriber_pc = PCTransport::new(
lk_runtime
.pc_factory
.create_peer_connection(rtc_config.clone())?,
SignalTarget::Subscriber,
);
let mut lossy_dc = publisher_pc.peer_connection().create_data_channel(
LOSSY_DC_LABEL,
DataChannelInit {
ordered: true,
max_retransmits: Some(0),
..DataChannelInit::default()
},
)?;
let mut reliable_dc = publisher_pc.peer_connection().create_data_channel(
RELIABLE_DC_LABEL,
DataChannelInit {
ordered: true,
..DataChannelInit::default()
},
)?;
// Forward events received in the Signaling Thread to our rtc channel
rtc_events::forward_pc_events(&mut publisher_pc, rtc_emitter.clone());
rtc_events::forward_pc_events(&mut subscriber_pc, rtc_emitter.clone());
rtc_events::forward_dc_events(&mut lossy_dc, rtc_emitter.clone());
rtc_events::forward_dc_events(&mut reliable_dc, rtc_emitter.clone());
let session_info = SessionInfo {
url: url.to_owned(),
token: token.to_owned(),
options,
join_response,
};
let (close_emitter, close_receiver) = watch::channel(false);
let inner = Arc::new(SessionInner {
info: session_info,
pc_state: AtomicU8::new(PCState::New as u8),
has_published: Default::default(),
signal_client,
publisher_pc: AsyncMutex::new(publisher_pc),
subscriber_pc: AsyncMutex::new(subscriber_pc),
lossy_dc,
reliable_dc,
subscriber_dc: Default::default(),
emitter: session_emitter,
});
// Start session tasks
let signal_task = tokio::spawn(
inner
.clone()
.signal_task(signal_events, close_receiver.clone()),
);
let rtc_task = tokio::spawn(inner.clone().rtc_task(rtc_events, close_receiver.clone()));
if !inner.info.join_response.subscriber_primary {
inner.negotiate_publisher().await?;
}
let session = Self {
lk_runtime,
inner: inner.clone(),
close_emitter,
signal_task,
rtc_task,
};
Ok(session)
}
/// Close the PeerConnections and the SignalClient
#[tracing::instrument]
pub async fn close(self) {
// Close the tasks
let _ = self.close_emitter.send(true);
let _ = self.rtc_task.await;
let _ = self.signal_task.await;
self.inner.close().await;
}
pub async fn publish_data(
&self,
data: &DataPacket,
kind: data_packet::Kind,
) -> Result<(), EngineError> {
self.inner.publish_data(data, kind).await
}
pub async fn restart(&self) -> EngineResult<()> {
self.inner.restart_session().await
}
pub async fn wait_pc_connection(&self) -> EngineResult<()> {
self.inner.wait_pc_connection().await
}
pub async fn simulate_scenario(&self, scenario: SimulateScenario) {
self.inner.simulate_scenario(scenario).await
}
}
impl RTCSession {
pub fn info(&self) -> &SessionInfo {
&self.inner.info
}
pub fn state(&self) -> PCState {
self.inner
.pc_state
.load(Ordering::SeqCst)
.try_into()
.unwrap()
}
pub fn publisher(&self) -> &AsyncMutex<PCTransport> {
&self.inner.publisher_pc
}
pub fn subscriber(&self) -> &AsyncMutex<PCTransport> {
&self.inner.subscriber_pc
}
pub fn signal_client(&self) -> &Arc<SignalClient> {
&self.inner.signal_client
}
pub fn data_channel(&self, kind: data_packet::Kind) -> &DataChannel {
&self.inner.data_channel(kind)
}
}
impl SessionInner {
async fn rtc_task(
self: Arc<Self>,
mut rtc_events: RTCEvents,
mut close_receiver: watch::Receiver<bool>,
) {
loop {
tokio::select! {
res = rtc_events.recv() => {
if let Some(event) = res {
if let Err(err) = self.on_rtc_event(event).await {
error!("failed to handle rtc event: {:?}", err);
}
} else {
panic!("rtc_events has been closed unexpectedly");
}
},
_ = close_receiver.changed() => {
break;
}
}
}
}
async fn signal_task(
self: Arc<Self>,
mut signal_events: SignalEvents,
mut close_receiver: watch::Receiver<bool>,
) {
loop {
tokio::select! {
res = signal_events.recv() => {
if let Some(signal) = res {
match signal {
SignalEvent::Open => {}
SignalEvent::Signal(signal) => {
if let Err(err) = self.on_signal_event(signal).await {
error!("failed to handle signal: {:?}", err);
}
}
SignalEvent::Close => {
self.on_session_disconnected("SignalClient closed", DisconnectReason::UnknownReason, true, false, false);
}
}
} else {
panic!("signal_events has been closed unexpectedly");
}
},
_ = close_receiver.changed() => {
break;
}
}
}
}
async fn on_signal_event(&self, event: signal_response::Message) -> EngineResult<()> {
match event {
signal_response::Message::Answer(answer) => {
trace!("received publisher answer: {:?}", answer);
let answer = SessionDescription::from(answer.r#type.parse().unwrap(), &answer.sdp)?;
self.publisher_pc
.lock()
.await
.set_remote_description(answer)
.await?;
}
signal_response::Message::Offer(offer) => {
trace!("received subscriber offer: {:?}", offer);
let offer = SessionDescription::from(offer.r#type.parse().unwrap(), &offer.sdp)?;
let answer = self
.subscriber_pc
.lock()
.await
.create_anwser(offer, RTCOfferAnswerOptions::default())
.await?;
self.signal_client
.send(signal_request::Message::Answer(proto::SessionDescription {
r#type: "answer".to_string(),
sdp: answer.to_string(),
}))
.await;
}
signal_response::Message::Trickle(trickle) => {
let target = SignalTarget::from_i32(trickle.target).unwrap();
let ice_candidate = {
let json = serde_json::from_str::<IceCandidateJSON>(&trickle.candidate_init)?;
IceCandidate::from(&json.sdpMid, json.sdpMLineIndex, &json.candidate)?
};
trace!("received ice_candidate {:?} {:?}", target, ice_candidate);
if target == SignalTarget::Publisher {
self.publisher_pc
.lock()
.await
.add_ice_candidate(ice_candidate)
.await?;
} else {
self.subscriber_pc
.lock()
.await
.add_ice_candidate(ice_candidate)
.await?;
}
}
signal_response::Message::Leave(leave) => {
self.on_session_disconnected(
"received leave",
leave.reason(),
leave.can_reconnect,
true,
true,
);
}
signal_response::Message::SpeakersChanged(speaker) => {
let _ = self.emitter.send(SessionEvent::SpeakersChanged {
speakers: speaker.speakers,
});
}
signal_response::Message::ConnectionQuality(quality) => {
let _ = self.emitter.send(SessionEvent::ConnectionQuality {
updates: quality.updates,
});
}
_ => {}
}
Ok(())
}
async fn on_rtc_event(&self, event: RTCEvent) -> EngineResult<()> {
match event {
RTCEvent::IceCandidate {
ice_candidate,
target,
} => {
self.signal_client
.send(signal_request::Message::Trickle(TrickleRequest {
candidate_init: serde_json::to_string(&IceCandidateJSON {
sdpMid: ice_candidate.sdp_mid(),
sdpMLineIndex: ice_candidate.sdp_mline_index(),
candidate: ice_candidate.candidate(),
})?,
target: target as i32,
}))
.await;
}
RTCEvent::ConnectionChange { state, target } => {
trace!("connection change, {:?} {:?}", state, target);
let is_primary = self.info.join_response.subscriber_primary
&& target == SignalTarget::Subscriber;
if is_primary && state == PeerConnectionState::Connected {
let old_state = self
.pc_state
.swap(PCState::Connected as u8, Ordering::SeqCst);
if old_state == PCState::New as u8 {
let _ = self.emitter.send(SessionEvent::Connected);
}
} else if state == PeerConnectionState::Failed {
self.pc_state
.store(PCState::Disconnected as u8, Ordering::SeqCst);
self.on_session_disconnected(
"pc_state failed",
DisconnectReason::UnknownReason,
true,
false,
false,
);
}
}
RTCEvent::DataChannel {
data_channel,
target: _,
} => {
self.subscriber_dc.lock().push(data_channel);
}
RTCEvent::Offer { offer, target: _ } => {
// Send the publisher offer to the server
self.signal_client
.send(signal_request::Message::Offer(proto::SessionDescription {
r#type: "offer".to_string(),
sdp: offer.to_string(),
}))
.await;
}
RTCEvent::AddTrack {
rtp_receiver,
mut streams,
target: _,
} => {
if !streams.is_empty() {
let _ = self.emitter.send(SessionEvent::MediaTrack {
track: rtp_receiver.track(),
stream: streams.remove(0),
receiver: rtp_receiver,
});
} else {
warn!("AddTrack event with no streams");
}
}
RTCEvent::Data { data, binary } => {
if !binary {
Err(EngineError::Internal(
"text messages aren't supported".to_string(),
))?;
}
let data = DataPacket::decode(&*data)?;
match data.value.unwrap() {
Value::User(user) => {
let _ = self.emitter.send(SessionEvent::Data {
participant_sid: user.participant_sid,
payload: user.payload,
kind: data_packet::Kind::from_i32(data.kind).unwrap(),
});
}
Value::Speaker(_) => {}
}
}
}
Ok(())
}
/// Called when the SignalClient or one of the PeerConnection has lost the connection
/// The RTCEngine may try a reconnect.
fn on_session_disconnected(
&self,
source: &str,
reason: DisconnectReason,
can_reconnect: bool,
retry_now: bool,
full_reconnect: bool,
) {
let _ = self.emitter.send(SessionEvent::Close {
source: source.to_owned(),
reason,
can_reconnect,
retry_now,
full_reconnect,
});
}
#[tracing::instrument]
async fn close(&self) {
self.signal_client.close().await;
self.publisher_pc.lock().await.close();
self.subscriber_pc.lock().await.close();
}
#[tracing::instrument]
async fn simulate_scenario(&self, scenario: SimulateScenario) {
match scenario {
SimulateScenario::SignalReconnect => {
self.signal_client.close().await;
}
SimulateScenario::Speaker => {
self.signal_client
.send(signal_request::Message::Simulate(proto::SimulateScenario {
scenario: Some(proto::simulate_scenario::Scenario::SpeakerUpdate(3)),
}))
.await;
}
SimulateScenario::NodeFailure => {
self.signal_client
.send(signal_request::Message::Simulate(proto::SimulateScenario {
scenario: Some(proto::simulate_scenario::Scenario::NodeFailure(true)),
}))
.await;
}
SimulateScenario::ServerLeave => {
self.signal_client
.send(signal_request::Message::Simulate(proto::SimulateScenario {
scenario: Some(proto::simulate_scenario::Scenario::ServerLeave(true)),
}))
.await;
}
SimulateScenario::Migration => {
self.signal_client
.send(signal_request::Message::Simulate(proto::SimulateScenario {
scenario: Some(proto::simulate_scenario::Scenario::Migration(true)),
}))
.await;
}
SimulateScenario::ForceTcp => {
self.signal_client
.send(signal_request::Message::Simulate(proto::SimulateScenario {
scenario: Some(
proto::simulate_scenario::Scenario::SwitchCandidateProtocol(
CandidateProtocol::Tcp as i32,
),
),
}))
.await;
}
SimulateScenario::ForceTls => {
self.signal_client
.send(signal_request::Message::Simulate(proto::SimulateScenario {
scenario: Some(
proto::simulate_scenario::Scenario::SwitchCandidateProtocol(
CandidateProtocol::Tls as i32,
),
),
}))
.await;
}
}
}
#[tracing::instrument(skip(data))]
async fn publish_data(
&self,
data: &DataPacket,
kind: data_packet::Kind,
) -> Result<(), EngineError> {
self.ensure_publisher_connected(kind).await?;
self.data_channel(kind)
.send(&data.encode_to_vec(), true)
.map_err(Into::into)
}
/// Try to restart the session by doing an ICE Restart (The SignalClient is also restarted)
/// This reconnection if more seemless than the full reconnection implemented in ['RTCEngine']
async fn restart_session(&self) -> EngineResult<()> {
self.signal_client.close().await;
let mut options = self.info.options.clone();
options.sid = self.info.join_response.participant.clone().unwrap().sid;
options.reconnect = true;
self.signal_client
.connect(&self.info.url, &self.info.token, options)
.await?;
self.subscriber_pc.lock().await.prepare_ice_restart();
if self.has_published.load(Ordering::Acquire) {
self.publisher_pc
.lock()
.await
.create_and_send_offer(RTCOfferAnswerOptions {
ice_restart: true,
..Default::default()
})
.await?;
}
self.wait_pc_connection().await?;
self.signal_client.flush_queue().await;
Ok(())
}
// Wait for PCState to become PCState::Connected
// Timeout after ['MAX_ICE_CONNECT_TIMEOUT']
async fn wait_pc_connection(&self) -> EngineResult<()> {
let wait_connected = async move {
while self.pc_state.load(Ordering::Acquire) != PCState::Connected as u8 {
tokio::task::yield_now().await;
}
};
tokio::select! {
_ = wait_connected => Ok(()),
_ = sleep(MAX_ICE_CONNECT_TIMEOUT) => {
let err = EngineError::Connection("wait_pc_connection timed out".to_string());
Err(err)
}
}
}
/// Start publisher negotiation
async fn negotiate_publisher(&self) -> EngineResult<()> {
self.has_published.store(true, Ordering::Release);
let res = self.publisher_pc.lock().await.negotiate().await;
if let Err(err) = &res {
error!("failed to negotiate the publisher: {:?}", err);
}
res.map_err(Into::into)
}
/// Ensure the Publisher PC is connected, if not, start the negotiation
/// This is required when sending data to the server
async fn ensure_publisher_connected(&self, kind: data_packet::Kind) -> EngineResult<()> {
if !self.info.join_response.subscriber_primary {
return Ok(());
}
if !self.publisher_pc.lock().await.is_connected()
&& self
.publisher_pc
.lock()
.await
.peer_connection()
.ice_connection_state()
!= IceConnectionState::IceConnectionChecking
{
let _ = self.negotiate_publisher().await;
}
let dc = self.data_channel(kind);
if dc.state() == DataState::Open {
return Ok(());
}
// Wait until the PeerConnection is connected
let wait_connected = async {
while self.publisher_pc.lock().await.is_connected() && dc.state() == DataState::Open {
tokio::task::yield_now().await;
}
};
// TODO(theomonnom) Avoid 15 seconds deadlock on the RTCEngine by recv close here
tokio::select! {
_ = wait_connected => Ok(()),
_ = sleep(MAX_ICE_CONNECT_TIMEOUT) => {
let err = EngineError::Connection("could not establish publisher connection: timeout".to_string());
error!(error = ?err);
Err(err)
}
}
}
fn data_channel(&self, kind: data_packet::Kind) -> &DataChannel {
if kind == data_packet::Kind::Reliable {
&self.reliable_dc
} else {
&self.lossy_dc
}
}
}