gst-meet/lib-gst-meet/src/conference.rs

1004 lines
38 KiB
Rust

use std::{
collections::HashMap, convert::TryFrom, fmt, future::Future, pin::Pin, sync::Arc, time::Duration,
};
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use colibri::{ColibriMessage, JsonMessage};
use futures::stream::StreamExt;
use glib::ObjectExt;
use gstreamer::prelude::{ElementExt, ElementExtManual, GstBinExt};
use jitsi_xmpp_parsers::jingle::{Action, Jingle};
use maplit::hashmap;
use once_cell::sync::Lazy;
use serde::Serialize;
use tokio::{
sync::{mpsc, oneshot, Mutex},
time,
};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, error, info, trace, warn};
use uuid::Uuid;
pub use xmpp_parsers::disco::Feature;
use xmpp_parsers::{
caps::{self, Caps},
disco::{DiscoInfoQuery, DiscoInfoResult, Identity},
ecaps2::{self, ECaps2},
hashes::{Algo, Hash},
iq::{Iq, IqType},
message::{Message, MessageType},
muc::{user::Status as MucStatus, Muc, MucUser},
nick::Nick,
ns,
presence::{self, Presence},
stanza_error::{DefinedCondition, ErrorType, StanzaError},
BareJid, FullJid, Jid,
};
use crate::{
colibri::ColibriChannel,
jingle::JingleSession,
source::MediaType,
stanza_filter::StanzaFilter,
util::generate_id,
xmpp::{self, connection::Connection},
};
const SEND_STATS_INTERVAL: Duration = Duration::from_secs(10);
const DISCO_NODE: &str = "https://github.com/avstack/gst-meet";
static DISCO_INFO: Lazy<DiscoInfoResult> = Lazy::new(|| DiscoInfoResult {
node: None,
identities: vec![Identity::new("client", "bot", "en", "gst-meet")],
features: vec![
Feature::new(ns::DISCO_INFO),
Feature::new(ns::JINGLE_RTP_AUDIO),
Feature::new(ns::JINGLE_RTP_VIDEO),
Feature::new(ns::JINGLE_ICE_UDP),
Feature::new(ns::JINGLE_DTLS),
Feature::new("urn:ietf:rfc:5888"), // BUNDLE
Feature::new("urn:ietf:rfc:5761"), // RTCP-MUX
Feature::new("urn:ietf:rfc:4588"), // RTX
Feature::new("http://jitsi.org/tcc"),
],
extensions: vec![],
});
static COMPUTED_CAPS_HASH: Lazy<Hash> =
Lazy::new(|| caps::hash_caps(&caps::compute_disco(&DISCO_INFO), Algo::Sha_1).unwrap());
#[derive(Debug, Clone, Copy)]
enum JitsiConferenceState {
Discovering,
JoiningMuc,
Idle,
}
#[derive(Debug, Clone)]
pub struct JitsiConferenceConfig {
pub muc: BareJid,
pub focus: Jid,
pub nick: String,
pub region: Option<String>,
pub video_codec: String,
pub extra_muc_features: Vec<String>,
pub start_bitrate: u32,
pub stereo: bool,
pub recv_video_scale_width: u16,
pub recv_video_scale_height: u16,
pub buffer_size: u32,
#[cfg(feature = "log-rtp")]
pub log_rtp: bool,
#[cfg(feature = "log-rtp")]
pub log_rtcp: bool,
}
#[derive(Clone)]
pub struct JitsiConference {
pub(crate) glib_main_context: glib::MainContext,
pub(crate) jid: FullJid,
pub(crate) xmpp_tx: mpsc::Sender<xmpp_parsers::Element>,
pub(crate) config: JitsiConferenceConfig,
pub(crate) external_services: Vec<xmpp::extdisco::Service>,
pub(crate) jingle_session: Arc<Mutex<Option<JingleSession>>>,
pub(crate) inner: Arc<Mutex<JitsiConferenceInner>>,
pub(crate) tls_insecure: bool,
}
impl fmt::Debug for JitsiConference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JitsiConference")
.field("jid", &self.jid)
.field("config", &self.config)
.field("inner", &self.inner)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct Participant {
pub jid: Option<FullJid>,
pub muc_jid: FullJid,
pub nick: Option<String>,
}
type BoxedResultFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
pub(crate) struct JitsiConferenceInner {
participants: HashMap<String, Participant>,
audio_sink: Option<gstreamer::Element>,
video_sink: Option<gstreamer::Element>,
on_participant:
Option<Arc<dyn (Fn(JitsiConference, Participant) -> BoxedResultFuture) + Send + Sync>>,
on_participant_left:
Option<Arc<dyn (Fn(JitsiConference, Participant) -> BoxedResultFuture) + Send + Sync>>,
on_colibri_message:
Option<Arc<dyn (Fn(JitsiConference, ColibriMessage) -> BoxedResultFuture) + Send + Sync>>,
presence: Vec<xmpp_parsers::Element>,
state: JitsiConferenceState,
send_resolution: Option<i32>,
connected_tx: Option<oneshot::Sender<()>>,
}
impl fmt::Debug for JitsiConferenceInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JitsiConferenceInner")
.field("state", &self.state)
.finish()
}
}
impl JitsiConference {
#[tracing::instrument(level = "debug", err)]
pub async fn join(
xmpp_connection: Connection,
glib_main_context: glib::MainContext,
config: JitsiConferenceConfig,
) -> Result<Self> {
let conference_stanza = xmpp::jitsi::Conference {
machine_uid: Uuid::new_v4().to_string(),
room: config.muc.to_string(),
properties: hashmap! {
"stereo".to_string() => config.stereo.to_string(),
"startBitrate".to_string() => config.start_bitrate.to_string(),
},
};
let (tx, rx) = oneshot::channel();
let focus = config.focus.clone();
let ecaps2_hash = ecaps2::hash_ecaps2(&ecaps2::compute_disco(&DISCO_INFO)?, Algo::Sha_256)?;
let mut presence = vec![
Muc::new().into(),
Caps::new(DISCO_NODE, COMPUTED_CAPS_HASH.clone()).into(),
ECaps2::new(vec![ecaps2_hash]).into(),
xmpp_parsers::Element::builder("stats-id", ns::DEFAULT_NS)
.append("gst-meet")
.build(),
xmpp_parsers::Element::builder("jitsi_participant_codecType", ns::DEFAULT_NS)
.append(config.video_codec.as_str())
.build(),
xmpp_parsers::Element::builder("audiomuted", ns::DEFAULT_NS)
.append("false")
.build(),
xmpp_parsers::Element::builder("videomuted", ns::DEFAULT_NS)
.append("false")
.build(),
xmpp_parsers::Element::builder("nick", "http://jabber.org/protocol/nick")
.append(config.nick.as_str())
.build(),
];
if let Some(region) = &config.region {
presence.extend([
xmpp_parsers::Element::builder("jitsi_participant_region", ns::DEFAULT_NS)
.append(region.as_str())
.build(),
xmpp_parsers::Element::builder("region", "http://jitsi.org/jitsi-meet")
.attr("id", region)
.build(),
]);
}
presence.extend(
config
.extra_muc_features
.iter()
.cloned()
.map(|var| Feature { var })
.map(|feature| feature.into()),
);
let conference = Self {
glib_main_context,
jid: xmpp_connection
.jid()
.await
.context("not connected (no JID)")?,
xmpp_tx: xmpp_connection.tx.clone(),
config,
external_services: xmpp_connection.external_services().await,
jingle_session: Arc::new(Mutex::new(None)),
inner: Arc::new(Mutex::new(JitsiConferenceInner {
state: JitsiConferenceState::Discovering,
presence,
participants: HashMap::new(),
audio_sink: None,
video_sink: None,
on_participant: None,
on_participant_left: None,
on_colibri_message: None,
send_resolution: None,
connected_tx: Some(tx),
})),
tls_insecure: xmpp_connection.tls_insecure,
};
xmpp_connection.add_stanza_filter(conference.clone()).await;
let iq = Iq::from_set(generate_id(), conference_stanza).with_to(focus);
xmpp_connection.tx.send(iq.into()).await?;
rx.await?;
Ok(conference)
}
#[tracing::instrument(level = "debug", err)]
pub async fn leave(self) -> Result<()> {
if let Some(jingle_session) = self.jingle_session.lock().await.take() {
debug!("pausing all sinks");
jingle_session.pause_all_sinks();
debug!("setting pipeline state to NULL");
if let Err(e) = jingle_session.pipeline().set_state(gstreamer::State::Null) {
warn!("failed to set pipeline state to NULL: {:?}", e);
}
debug!("waiting for state change to complete");
let _ = jingle_session.pipeline_stopped().await;
}
// should leave the XMPP muc gracefully, instead of just disconnecting
Ok(())
}
fn endpoint_id(&self) -> Result<&str> {
self
.jid
.node
.as_ref()
.ok_or_else(|| anyhow!("invalid jid"))?
.split('-')
.next()
.ok_or_else(|| anyhow!("invalid jid"))
}
fn jid_in_muc(&self) -> Result<FullJid> {
Ok(self.config.muc.clone().with_resource(self.endpoint_id()?))
}
pub(crate) fn focus_jid_in_muc(&self) -> Result<FullJid> {
Ok(self.config.muc.clone().with_resource("focus"))
}
#[tracing::instrument(level = "debug", err)]
async fn send_presence(&self, payloads: &[xmpp_parsers::Element]) -> Result<()> {
let mut presence = Presence::new(presence::Type::None).with_to(self.jid_in_muc()?);
presence.payloads = payloads.to_owned();
self.xmpp_tx.send(presence.into()).await?;
Ok(())
}
#[tracing::instrument(level = "debug", err)]
pub async fn set_muted(&self, media_type: MediaType, muted: bool) -> Result<()> {
let mut locked_inner = self.inner.lock().await;
let element = xmpp_parsers::Element::builder(
media_type.jitsi_muted_presence_element_name(),
ns::DEFAULT_NS,
)
.append(muted.to_string())
.build();
locked_inner
.presence
.retain(|el| el.name() != media_type.jitsi_muted_presence_element_name());
locked_inner.presence.push(element);
self.send_presence(&locked_inner.presence).await
}
pub async fn pipeline(&self) -> Result<gstreamer::Pipeline> {
Ok(
self
.jingle_session
.lock()
.await
.as_ref()
.context("not connected (no jingle session)")?
.pipeline(),
)
}
#[tracing::instrument(level = "debug", err)]
pub async fn add_bin(&self, bin: &gstreamer::Bin) -> Result<()> {
let pipeline = self.pipeline().await?;
pipeline.add(bin)?;
bin.sync_state_with_parent()?;
Ok(())
}
#[tracing::instrument(level = "debug", err)]
pub async fn set_pipeline_state(&self, state: gstreamer::State) -> Result<()> {
self.pipeline().await?.call_async(move |p| {
if let Err(e) = p.set_state(state) {
error!("pipeline set_state: {:?}", e);
}
});
Ok(())
}
pub async fn remote_participant_audio_sink_element(&self) -> Option<gstreamer::Element> {
self.inner.lock().await.audio_sink.as_ref().cloned()
}
pub async fn set_remote_participant_audio_sink_element(&self, sink: Option<gstreamer::Element>) {
self.inner.lock().await.audio_sink = sink;
}
pub async fn remote_participant_video_sink_element(&self) -> Option<gstreamer::Element> {
self.inner.lock().await.video_sink.as_ref().cloned()
}
pub async fn set_remote_participant_video_sink_element(&self, sink: Option<gstreamer::Element>) {
self.inner.lock().await.video_sink = sink;
}
pub async fn audio_sink_element(&self) -> Result<gstreamer::Element> {
Ok(
self
.jingle_session
.lock()
.await
.as_ref()
.context("not connected (no jingle session)")?
.audio_sink_element(),
)
}
pub async fn video_sink_element(&self) -> Result<gstreamer::Element> {
Ok(
self
.jingle_session
.lock()
.await
.as_ref()
.context("not connected (no jingle session)")?
.video_sink_element(),
)
}
/// Set the max resolution that we are currently sending.
///
/// Setting this is required for browser clients in the same conference to display
/// the stats that we broadcast.
///
/// Note that lib-gst-meet does not encode video (that is the responsibility of your
/// GStreamer pipeline), so this is purely informational.
pub async fn set_send_resolution(&self, height: i32) {
self.inner.lock().await.send_resolution = Some(height);
}
pub async fn send_colibri_message(&self, message: ColibriMessage) -> Result<()> {
self
.jingle_session
.lock()
.await
.as_ref()
.context("not connected (no jingle session)")?
.colibri_channel
.as_ref()
.context("no colibri channel")?
.send(message)
.await
}
pub async fn send_json_message<T: Serialize>(&self, payload: &T) -> Result<()> {
let message = Message {
from: Some(Jid::Full(self.jid.clone())),
to: Some(Jid::Bare(self.config.muc.clone())),
id: Some(Uuid::new_v4().to_string()),
type_: MessageType::Groupchat,
bodies: Default::default(),
subjects: Default::default(),
thread: None,
payloads: vec![xmpp_parsers::Element::try_from(xmpp::jitsi::JsonMessage {
payload: serde_json::to_value(payload)?,
})?],
};
self.xmpp_tx.send(message.into()).await?;
Ok(())
}
pub(crate) async fn ensure_participant(&self, id: &str) -> Result<()> {
if !self.inner.lock().await.participants.contains_key(id) {
let participant = Participant {
jid: None,
muc_jid: self.config.muc.clone().with_resource(id),
nick: None,
};
self
.inner
.lock()
.await
.participants
.insert(id.to_owned(), participant.clone());
if let Some(f) = self.inner.lock().await.on_participant.as_ref().cloned() {
if let Err(e) = f(self.clone(), participant.clone()).await {
warn!("on_participant failed: {:?}", e);
}
else if let Ok(pipeline) = self.pipeline().await {
gstreamer::debug_bin_to_dot_file(
&pipeline,
gstreamer::DebugGraphDetails::ALL,
&format!("participant-added-{}", participant.muc_jid.resource),
);
}
}
}
Ok(())
}
#[tracing::instrument(level = "trace", skip(f))]
pub async fn on_participant(
&self,
f: impl (Fn(JitsiConference, Participant) -> BoxedResultFuture) + Send + Sync + 'static,
) {
let f = Arc::new(f);
let f2 = f.clone();
let existing_participants: Vec<_> = {
let mut locked_inner = self.inner.lock().await;
locked_inner.on_participant = Some(f2);
locked_inner.participants.values().cloned().collect()
};
for participant in existing_participants {
debug!(
"calling on_participant with existing participant: {:?}",
participant
);
if let Err(e) = f(self.clone(), participant.clone()).await {
warn!("on_participant failed: {:?}", e);
}
else if let Ok(pipeline) = self.pipeline().await {
gstreamer::debug_bin_to_dot_file(
&pipeline,
gstreamer::DebugGraphDetails::ALL,
&format!("participant-added-{}", participant.muc_jid.resource),
);
}
}
}
#[tracing::instrument(level = "trace", skip(f))]
pub async fn on_participant_left(
&self,
f: impl (Fn(JitsiConference, Participant) -> BoxedResultFuture) + Send + Sync + 'static,
) {
self.inner.lock().await.on_participant_left = Some(Arc::new(f));
}
#[tracing::instrument(level = "trace", skip(f))]
pub async fn on_colibri_message(
&self,
f: impl (Fn(JitsiConference, ColibriMessage) -> BoxedResultFuture) + Send + Sync + 'static,
) {
self.inner.lock().await.on_colibri_message = Some(Arc::new(f));
}
}
#[async_trait]
impl StanzaFilter for JitsiConference {
#[tracing::instrument(level = "trace")]
fn filter(&self, element: &xmpp_parsers::Element) -> bool {
element.attr("from") == Some(self.config.focus.to_string().as_str())
&& element.is("iq", ns::DEFAULT_NS)
|| element
.attr("from")
.and_then(|from| from.parse::<BareJid>().ok())
.map(|jid| jid == self.config.muc)
.unwrap_or_default()
&& (element.is("presence", ns::DEFAULT_NS) || element.is("iq", ns::DEFAULT_NS))
}
#[tracing::instrument(level = "trace", err)]
async fn take(&self, element: xmpp_parsers::Element) -> Result<()> {
use JitsiConferenceState::*;
let state = self.inner.lock().await.state;
match state {
Discovering => {
let iq = Iq::try_from(element)?;
if let IqType::Result(Some(element)) = iq.payload {
let ready: bool = element
.attr("ready")
.ok_or_else(|| anyhow!("missing ready attribute on conference IQ"))?
.parse()?;
if !ready {
bail!("focus reports room not ready");
}
}
else {
bail!("focus IQ failed");
};
let mut locked_inner = self.inner.lock().await;
self.send_presence(&locked_inner.presence).await?;
locked_inner.state = JoiningMuc;
},
JoiningMuc => {
let presence = Presence::try_from(element)?;
if let Some(payload) = presence
.payloads
.iter()
.find(|payload| payload.is("x", ns::MUC_USER))
{
let muc_user = MucUser::try_from(payload.clone())?;
if muc_user.status.contains(&MucStatus::SelfPresence) {
debug!("Joined MUC: {}", self.config.muc);
self.inner.lock().await.state = Idle;
}
}
},
Idle => {
if let Ok(iq) = Iq::try_from(element.clone()) {
match iq.payload {
IqType::Get(element) => {
if let Ok(query) = DiscoInfoQuery::try_from(element) {
debug!(
"Received disco info query from {} for node {:?}",
iq.from.as_ref().unwrap(),
query.node
);
if let Some(node) = query.node {
match node.splitn(2, '#').collect::<Vec<_>>().as_slice() {
// TODO: also support ecaps2, as we send it in our presence.
[uri, hash]
if *uri == DISCO_NODE && *hash == COMPUTED_CAPS_HASH.to_base64() =>
{
let mut disco_info = DISCO_INFO.clone();
disco_info.node = Some(node);
let iq = Iq::from_result(iq.id, Some(disco_info))
.with_from(Jid::Full(self.jid.clone()))
.with_to(iq.from.unwrap());
self.xmpp_tx.send(iq.into()).await?;
},
_ => {
let error = StanzaError::new(
ErrorType::Cancel,
DefinedCondition::ItemNotFound,
"en",
format!("Unknown disco#info node: {}", node),
);
let iq = Iq::from_error(iq.id, error)
.with_from(Jid::Full(self.jid.clone()))
.with_to(iq.from.unwrap());
self.xmpp_tx.send(iq.into()).await?;
},
}
}
else {
let iq = Iq::from_result(iq.id, Some(DISCO_INFO.clone()))
.with_from(Jid::Full(self.jid.clone()))
.with_to(iq.from.unwrap());
self.xmpp_tx.send(iq.into()).await?;
}
}
},
IqType::Set(element) => match Jingle::try_from(element) {
Ok(jingle) => {
if let Some(Jid::Full(from_jid)) = iq.from {
if jingle.action == Action::SessionInitiate {
if from_jid.resource == "focus" {
// Acknowledge the IQ
let result_iq = Iq::empty_result(Jid::Full(from_jid.clone()), iq.id.clone())
.with_from(Jid::Full(self.jid.clone()));
self.xmpp_tx.send(result_iq.into()).await?;
*self.jingle_session.lock().await =
Some(JingleSession::initiate(self, jingle).await?);
}
else {
debug!("Ignored Jingle session-initiate from {}", from_jid);
}
}
else if jingle.action == Action::SourceAdd {
debug!("Received Jingle source-add");
// Acknowledge the IQ
let result_iq = Iq::empty_result(Jid::Full(from_jid.clone()), iq.id.clone())
.with_from(Jid::Full(self.jid.clone()));
self.xmpp_tx.send(result_iq.into()).await?;
self
.jingle_session
.lock()
.await
.as_mut()
.context("not connected (no jingle session")?
.source_add(jingle)
.await?;
}
}
else {
debug!("Received Jingle IQ from invalid JID: {:?}", iq.from);
}
},
Err(e) => debug!("IQ did not successfully parse as Jingle: {:?}", e),
},
IqType::Result(_) => {
if let Some(jingle_session) = self.jingle_session.lock().await.as_mut() {
if Some(iq.id) == jingle_session.accept_iq_id {
let colibri_url = jingle_session.colibri_url.clone();
jingle_session.accept_iq_id = None;
debug!("Focus acknowledged session-accept");
if let Some(colibri_url) = colibri_url {
info!("Connecting Colibri WebSocket to {}", colibri_url);
let colibri_channel =
ColibriChannel::new(&colibri_url, self.tls_insecure).await?;
let (tx, rx) = mpsc::channel(8);
colibri_channel.subscribe(tx).await;
jingle_session.colibri_channel = Some(colibri_channel.clone());
let my_endpoint_id = self.endpoint_id()?.to_owned();
{
let my_endpoint_id = my_endpoint_id.clone();
let colibri_channel = colibri_channel.clone();
let self_ = self.clone();
jingle_session.stats_handler_task = Some(tokio::spawn(async move {
let mut interval = time::interval(SEND_STATS_INTERVAL);
loop {
let maybe_remote_ssrc_map = self_
.jingle_session
.lock()
.await
.as_ref()
.map(|sess| sess.remote_ssrc_map.clone());
let maybe_source_stats: Option<Vec<gstreamer::Structure>> = self_
.pipeline()
.await
.ok()
.and_then(|pipeline| pipeline.by_name("rtpbin"))
.and_then(|rtpbin| {
rtpbin.try_emit_by_name("get-session", &[&0u32]).ok()
})
.and_then(|rtpsession: gstreamer::Element| {
rtpsession.try_property("stats").ok()
})
.and_then(|stats: gstreamer::Structure| stats.get("source-stats").ok())
.and_then(|stats: glib::ValueArray| {
stats
.into_iter()
.map(|v| v.get())
.collect::<Result<_, _>>()
.ok()
});
if let (Some(remote_ssrc_map), Some(source_stats)) =
(maybe_remote_ssrc_map, maybe_source_stats)
{
debug!("source stats: {:#?}", source_stats);
let audio_recv_bitrate: u64 = source_stats
.iter()
.filter(|stat| {
stat
.get("ssrc")
.ok()
.and_then(|ssrc: u32| remote_ssrc_map.get(&ssrc))
.map(|source| {
source.media_type == MediaType::Audio
&& source
.participant_id
.as_ref()
.map(|id| id != &my_endpoint_id)
.unwrap_or_default()
})
.unwrap_or_default()
})
.filter_map(|stat| stat.get::<u64>("bitrate").ok())
.sum();
let video_recv_bitrate: u64 = source_stats
.iter()
.filter(|stat| {
stat
.get("ssrc")
.ok()
.and_then(|ssrc: u32| remote_ssrc_map.get(&ssrc))
.map(|source| {
source.media_type == MediaType::Video
&& source
.participant_id
.as_ref()
.map(|id| id != &my_endpoint_id)
.unwrap_or_default()
})
.unwrap_or_default()
})
.filter_map(|stat| stat.get::<u64>("bitrate").ok())
.sum();
let audio_send_bitrate: u64 = source_stats
.iter()
.find(|stat| {
stat
.get("ssrc")
.ok()
.and_then(|ssrc: u32| remote_ssrc_map.get(&ssrc))
.map(|source| {
source.media_type == MediaType::Audio
&& source
.participant_id
.as_ref()
.map(|id| id == &my_endpoint_id)
.unwrap_or_default()
})
.unwrap_or_default()
})
.and_then(|stat| stat.get("bitrate").ok())
.unwrap_or_default();
let video_send_bitrate: u64 = source_stats
.iter()
.find(|stat| {
stat
.get("ssrc")
.ok()
.and_then(|ssrc: u32| remote_ssrc_map.get(&ssrc))
.map(|source| {
source.media_type == MediaType::Video
&& source
.participant_id
.as_ref()
.map(|id| id == &my_endpoint_id)
.unwrap_or_default()
})
.unwrap_or_default()
})
.and_then(|stat| stat.get("bitrate").ok())
.unwrap_or_default();
let recv_packets: u64 = source_stats
.iter()
.filter(|stat| {
stat
.get("ssrc")
.ok()
.and_then(|ssrc: u32| remote_ssrc_map.get(&ssrc))
.map(|source| {
source
.participant_id
.as_ref()
.map(|id| id != &my_endpoint_id)
.unwrap_or_default()
})
.unwrap_or_default()
})
.filter_map(|stat| stat.get::<u64>("packets-received").ok())
.sum();
let recv_lost: u64 = source_stats
.iter()
.filter(|stat| {
stat
.get("ssrc")
.ok()
.and_then(|ssrc: u32| remote_ssrc_map.get(&ssrc))
.map(|source| source.participant_id.as_ref().map(|id| id != &my_endpoint_id).unwrap_or_default())
.unwrap_or_default()
})
.filter_map(|stat| stat.get::<i32>("packets-lost").ok())
.sum::<i32>()
// Loss can be negative because of duplicate packets. Clamp it to zero.
.try_into()
.unwrap_or_default();
let recv_loss =
recv_lost as f64 / (recv_packets as f64 + recv_lost as f64);
let stats = ColibriMessage::EndpointStats {
from: None,
bitrate: colibri::Bitrates {
audio: colibri::Bitrate {
upload: audio_send_bitrate / 1024,
download: audio_recv_bitrate / 1024,
},
video: colibri::Bitrate {
upload: video_send_bitrate / 1024,
download: video_recv_bitrate / 1024,
},
total: colibri::Bitrate {
upload: (audio_send_bitrate + video_send_bitrate) / 1024,
download: (audio_recv_bitrate + video_recv_bitrate) / 1024,
},
},
packet_loss: colibri::PacketLoss {
total: (recv_loss * 100.) as u64,
download: (recv_loss * 100.) as u64,
upload: 0, // TODO
},
connection_quality: 100.0,
jvb_rtt: Some(0), // TODO
server_region: self_.config.region.clone(),
max_enabled_resolution: self_.inner.lock().await.send_resolution,
};
if let Err(e) = colibri_channel.send(stats).await {
warn!("failed to send stats: {:?}", e);
}
}
else {
warn!("unable to get stats from pipeline");
}
interval.tick().await;
}
}));
}
{
let self_ = self.clone();
tokio::spawn(async move {
let mut stream = ReceiverStream::new(rx);
while let Some(msg) = stream.next().await {
// Some message types are handled internally rather than passed to the on_colibri_message handler.
let handled = match &msg {
ColibriMessage::EndpointMessage {
to: Some(to),
from,
msg_payload,
} if to == &my_endpoint_id => {
match serde_json::from_value::<JsonMessage>(msg_payload.clone()) {
Ok(JsonMessage::E2ePingRequest { id }) => {
if let Err(e) = colibri_channel
.send(ColibriMessage::EndpointMessage {
from: None,
to: from.clone(),
msg_payload: serde_json::to_value(
JsonMessage::E2ePingResponse { id },
)
.unwrap(),
})
.await
{
warn!("failed to send e2e ping response: {:?}", e);
}
true
},
_ => false,
}
},
_ => false,
};
if handled {
continue;
}
let locked_inner = self_.inner.lock().await;
if let Some(f) = &locked_inner.on_colibri_message {
if let Err(e) = f(self_.clone(), msg).await {
warn!("on_colibri_message failed: {:?}", e);
}
}
}
Ok::<_, anyhow::Error>(())
});
}
}
if let Some(connected_tx) = self.inner.lock().await.connected_tx.take() {
connected_tx.send(()).unwrap();
}
}
}
},
_ => {},
}
}
else if let Ok(presence) = Presence::try_from(element) {
if let Jid::Full(from) = presence
.from
.as_ref()
.context("missing from in presence")?
.clone()
{
let bare_from: BareJid = from.clone().into();
if bare_from == self.config.muc && from.resource != "focus" {
trace!("received MUC presence from {}", from.resource);
let nick_payload = presence
.payloads
.iter()
.find(|e| e.is("nick", ns::NICK))
.map(|e| Nick::try_from(e.clone()))
.transpose()?;
if let Some(muc_user_payload) = presence
.payloads
.into_iter()
.find(|e| e.is("x", ns::MUC_USER))
{
let muc_user = MucUser::try_from(muc_user_payload)?;
for item in muc_user.items {
if let Some(jid) = &item.jid {
if jid == &self.jid {
continue;
}
let participant = Participant {
jid: Some(jid.clone()),
muc_jid: from.clone(),
nick: item
.nick
.or_else(|| nick_payload.as_ref().map(|nick| nick.0.clone())),
};
if presence.type_ == presence::Type::Unavailable
&& self
.inner
.lock()
.await
.participants
.remove(&from.resource.clone())
.is_some()
{
debug!("participant left: {:?}", jid);
if let Some(f) = &self
.inner
.lock()
.await
.on_participant_left
.as_ref()
.cloned()
{
debug!("calling on_participant_left with old participant");
if let Err(e) = f(self.clone(), participant).await {
warn!("on_participant_left failed: {:?}", e);
}
}
}
else if self
.inner
.lock()
.await
.participants
.insert(from.resource.clone(), participant.clone())
.is_none()
{
debug!("new participant: {:?}", jid);
if let Some(f) = &self.inner.lock().await.on_participant.as_ref().cloned() {
debug!("calling on_participant with new participant");
if let Err(e) = f(self.clone(), participant.clone()).await {
warn!("on_participant failed: {:?}", e);
}
else if let Some(jingle_session) =
self.jingle_session.lock().await.as_ref()
{
gstreamer::debug_bin_to_dot_file(
&jingle_session.pipeline(),
gstreamer::DebugGraphDetails::ALL,
&format!("participant-added-{}", participant.muc_jid.resource),
);
}
}
}
}
}
}
}
}
}
},
}
Ok(())
}
}