XMPP improvements

- Handle unsolicited XMPP messages gracefully
- Split out generic XMPP connection handler which can be used for
  connecting to brewery MUCs
- Don't deadlock during Jingle handling
This commit is contained in:
Jasper Hugo 2021-09-07 23:52:04 +07:00
parent 5759d5c800
commit 037c2d944f
13 changed files with 458 additions and 317 deletions

2
Cargo.lock generated
View File

@ -1829,8 +1829,6 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "xmpp-parsers-gst-meet" name = "xmpp-parsers-gst-meet"
version = "0.18.2" version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cde8e9611ca7cac569119e4ec1b6fe50da4df91a168fdab786693029ab1482a"
dependencies = [ dependencies = [
"base64", "base64",
"blake2", "blake2",

View File

@ -27,6 +27,7 @@ rcgen = { version = "0.8", default-features = false }
ring = { version = "0.16", default-features = false } ring = { version = "0.16", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] } serde = { version = "1", default-features = false, features = ["derive"] }
serde_json = { version = "1", default-features = false, features = ["std"] } serde_json = { version = "1", default-features = false, features = ["std"] }
serde_with = { version = "1", default-features = false, features = ["macros"] }
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "sync", "time"] } tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros", "sync", "time"] }
tokio-stream = { version = "0.1", default-features = false, features = ["time"] } tokio-stream = { version = "0.1", default-features = false, features = ["time"] }
tokio-tungstenite = { version = "0.14", default-features = false, features = ["connect", "rustls-tls"] } tokio-tungstenite = { version = "0.14", default-features = false, features = ["connect", "rustls-tls"] }
@ -39,7 +40,7 @@ tracing-subscriber = { version = "0.2", optional = true, default-features = fals
"tracing-log", "tracing-log",
] } ] }
uuid = { version = "0.8", default-features = false, features = ["v4"] } uuid = { version = "0.8", default-features = false, features = ["v4"] }
xmpp-parsers = { package = "xmpp-parsers-gst-meet", version = "0.18", default-features = false } xmpp-parsers = { path = "../../xmpp-rs/xmpp-parsers", package = "xmpp-parsers-gst-meet", version = "0.18", default-features = false }
[features] [features]
default = [] default = []

View File

@ -6,11 +6,13 @@ use futures::{
stream::{StreamExt, TryStreamExt}, stream::{StreamExt, TryStreamExt},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tokio_tungstenite::tungstenite::{http::Request, Message}; use tokio_tungstenite::tungstenite::{http::Request, Message};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "colibriClass")] #[serde(tag = "colibriClass")]
pub enum ColibriMessage { pub enum ColibriMessage {
@ -20,7 +22,11 @@ pub enum ColibriMessage {
previous_speakers: Vec<String>, previous_speakers: Vec<String>,
}, },
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
EndpointConnectivityStatusChangeEvent { endpoint: String, active: bool }, EndpointConnectivityStatusChangeEvent {
endpoint: String,
#[serde_as(as = "DisplayFromStr")]
active: bool,
},
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
EndpointMessage { EndpointMessage {
from: String, from: String,

View File

@ -4,17 +4,23 @@ use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use gstreamer::prelude::{ElementExt, ElementExtManual, GstBinExt}; use gstreamer::prelude::{ElementExt, ElementExtManual, GstBinExt};
use maplit::hashmap;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Serialize;
use tokio::sync::{mpsc, oneshot, Mutex}; use tokio::sync::{mpsc, oneshot, Mutex};
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tracing::{debug, error, info, trace, warn}; use tracing::{debug, error, info, trace, warn};
use uuid::Uuid;
pub use xmpp_parsers::disco::Feature;
use xmpp_parsers::{ use xmpp_parsers::{
disco::{DiscoInfoQuery, DiscoInfoResult, Feature}, disco::{DiscoInfoQuery, DiscoInfoResult},
ecaps2::{self, ECaps2}, ecaps2::{self, ECaps2},
hashes::Algo, hashes::Algo,
iq::{Iq, IqType}, iq::{Iq, IqType},
jingle::{Action, Jingle}, jingle::{Action, Jingle},
message::{Message, MessageType},
muc::{Muc, MucUser}, muc::{Muc, MucUser},
nick::Nick,
ns, ns,
presence::{self, Presence}, presence::{self, Presence},
BareJid, Element, FullJid, Jid, BareJid, Element, FullJid, Jid,
@ -25,7 +31,8 @@ use crate::{
jingle::JingleSession, jingle::JingleSession,
source::MediaType, source::MediaType,
stanza_filter::StanzaFilter, stanza_filter::StanzaFilter,
xmpp, util::generate_id,
xmpp::{self, connection::Connection},
}; };
static DISCO_INFO: Lazy<DiscoInfoResult> = Lazy::new(|| { static DISCO_INFO: Lazy<DiscoInfoResult> = Lazy::new(|| {
@ -70,6 +77,7 @@ pub struct JitsiConferenceConfig {
pub nick: String, pub nick: String,
pub region: String, pub region: String,
pub video_codec: String, pub video_codec: String,
pub extra_muc_features: Vec<String>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -79,6 +87,7 @@ pub struct JitsiConference {
pub(crate) xmpp_tx: mpsc::Sender<Element>, pub(crate) xmpp_tx: mpsc::Sender<Element>,
pub(crate) config: JitsiConferenceConfig, pub(crate) config: JitsiConferenceConfig,
pub(crate) external_services: Vec<xmpp::extdisco::Service>, pub(crate) external_services: Vec<xmpp::extdisco::Service>,
pub(crate) jingle_session: Arc<Mutex<Option<JingleSession>>>,
pub(crate) inner: Arc<Mutex<JitsiConferenceInner>>, pub(crate) inner: Arc<Mutex<JitsiConferenceInner>>,
} }
@ -94,7 +103,7 @@ impl fmt::Debug for JitsiConference {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Participant { pub struct Participant {
pub jid: FullJid, pub jid: Option<FullJid>,
pub muc_jid: FullJid, pub muc_jid: FullJid,
pub nick: Option<String>, pub nick: Option<String>,
} }
@ -102,7 +111,6 @@ pub struct Participant {
type BoxedResultFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>; type BoxedResultFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
pub(crate) struct JitsiConferenceInner { pub(crate) struct JitsiConferenceInner {
pub(crate) jingle_session: Option<JingleSession>,
participants: HashMap<String, Participant>, participants: HashMap<String, Participant>,
on_participant: on_participant:
Option<Arc<dyn (Fn(JitsiConference, Participant) -> BoxedResultFuture) + Send + Sync>>, Option<Arc<dyn (Fn(JitsiConference, Participant) -> BoxedResultFuture) + Send + Sync>>,
@ -112,7 +120,6 @@ pub(crate) struct JitsiConferenceInner {
Option<Arc<dyn (Fn(JitsiConference, ColibriMessage) -> BoxedResultFuture) + Send + Sync>>, Option<Arc<dyn (Fn(JitsiConference, ColibriMessage) -> BoxedResultFuture) + Send + Sync>>,
state: JitsiConferenceState, state: JitsiConferenceState,
connected_tx: Option<oneshot::Sender<()>>, connected_tx: Option<oneshot::Sender<()>>,
connected_rx: Option<oneshot::Receiver<()>>,
} }
impl fmt::Debug for JitsiConferenceInner { impl fmt::Debug for JitsiConferenceInner {
@ -124,51 +131,60 @@ impl fmt::Debug for JitsiConferenceInner {
} }
impl JitsiConference { impl JitsiConference {
#[tracing::instrument(level = "debug", skip(xmpp_tx), err)] #[tracing::instrument(level = "debug", err)]
pub(crate) async fn new( pub async fn join(
xmpp_connection: Connection,
glib_main_context: glib::MainContext, glib_main_context: glib::MainContext,
jid: FullJid,
xmpp_tx: mpsc::Sender<Element>,
config: JitsiConferenceConfig, config: JitsiConferenceConfig,
external_services: Vec<xmpp::extdisco::Service>,
) -> Result<Self> { ) -> Result<Self> {
let conference_stanza = xmpp::jitsi::Conference {
machine_uid: Uuid::new_v4().to_string(),
room: config.muc.to_string(),
properties: hashmap! {
// Disable voice processing
// TODO put this in config
"stereo".to_string() => "true".to_string(),
"startBitrate".to_string() => "800".to_string(),
},
};
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
Ok(Self {
let focus = config.focus.clone();
let conference = Self {
glib_main_context, glib_main_context,
jid, jid: xmpp_connection
xmpp_tx, .jid()
.await
.context("not connected (no JID)")?,
xmpp_tx: xmpp_connection.tx.clone(),
config, config,
external_services, external_services: xmpp_connection.external_services().await,
jingle_session: Arc::new(Mutex::new(None)),
inner: Arc::new(Mutex::new(JitsiConferenceInner { inner: Arc::new(Mutex::new(JitsiConferenceInner {
state: JitsiConferenceState::Discovering, state: JitsiConferenceState::Discovering,
participants: HashMap::new(), participants: HashMap::new(),
on_participant: None, on_participant: None,
on_participant_left: None, on_participant_left: None,
on_colibri_message: None, on_colibri_message: None,
jingle_session: None,
connected_tx: Some(tx), connected_tx: Some(tx),
connected_rx: Some(rx),
})), })),
})
}
pub(crate) async fn connected(&self) -> Result<()> {
let rx = {
let mut locked_inner = self.inner.lock().await;
locked_inner
.connected_rx
.take()
.context("connected() called twice")?
}; };
xmpp_connection.add_stanza_filter(conference.clone()).await;
let iq = Iq::from_set(generate_id(), conference_stanza).with_to(Jid::Full(focus));
xmpp_connection.tx.send(iq.into()).await?;
rx.await?; rx.await?;
Ok(())
Ok(conference)
} }
#[tracing::instrument(level = "debug", err)] #[tracing::instrument(level = "debug", err)]
pub async fn leave(self) -> Result<()> { pub async fn leave(self) -> Result<()> {
let mut inner = self.inner.lock().await; if let Some(jingle_session) = self.jingle_session.lock().await.take() {
if let Some(jingle_session) = inner.jingle_session.take() {
debug!("pausing all sinks"); debug!("pausing all sinks");
jingle_session.pause_all_sinks(); jingle_session.pause_all_sinks();
@ -224,10 +240,9 @@ impl JitsiConference {
pub async fn pipeline(&self) -> Result<gstreamer::Pipeline> { pub async fn pipeline(&self) -> Result<gstreamer::Pipeline> {
Ok( Ok(
self self
.inner .jingle_session
.lock() .lock()
.await .await
.jingle_session
.as_ref() .as_ref()
.context("not connected (no jingle session)")? .context("not connected (no jingle session)")?
.pipeline(), .pipeline(),
@ -255,10 +270,9 @@ impl JitsiConference {
pub async fn audio_sink_element(&self) -> Result<gstreamer::Element> { pub async fn audio_sink_element(&self) -> Result<gstreamer::Element> {
Ok( Ok(
self self
.inner .jingle_session
.lock() .lock()
.await .await
.jingle_session
.as_ref() .as_ref()
.context("not connected (no jingle session)")? .context("not connected (no jingle session)")?
.audio_sink_element(), .audio_sink_element(),
@ -268,10 +282,9 @@ impl JitsiConference {
pub async fn video_sink_element(&self) -> Result<gstreamer::Element> { pub async fn video_sink_element(&self) -> Result<gstreamer::Element> {
Ok( Ok(
self self
.inner .jingle_session
.lock() .lock()
.await .await
.jingle_session
.as_ref() .as_ref()
.context("not connected (no jingle session)")? .context("not connected (no jingle session)")?
.video_sink_element(), .video_sink_element(),
@ -280,10 +293,9 @@ impl JitsiConference {
pub async fn send_colibri_message(&self, message: ColibriMessage) -> Result<()> { pub async fn send_colibri_message(&self, message: ColibriMessage) -> Result<()> {
self self
.inner .jingle_session
.lock() .lock()
.await .await
.jingle_session
.as_ref() .as_ref()
.context("not connected (no jingle session)")? .context("not connected (no jingle session)")?
.colibri_channel .colibri_channel
@ -293,6 +305,54 @@ impl JitsiConference {
.await .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![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))] #[tracing::instrument(level = "trace", skip(f))]
pub async fn on_participant( pub async fn on_participant(
&self, &self,
@ -313,6 +373,15 @@ impl JitsiConference {
if let Err(e) = f(self.clone(), participant.clone()).await { if let Err(e) = f(self.clone(), participant.clone()).await {
warn!("on_participant failed: {:?}", e); 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),
);
}
}
} }
} }
@ -349,10 +418,9 @@ impl StanzaFilter for JitsiConference {
#[tracing::instrument(level = "trace", err)] #[tracing::instrument(level = "trace", err)]
async fn take(&self, element: Element) -> Result<()> { async fn take(&self, element: Element) -> Result<()> {
let mut locked_inner = self.inner.lock().await;
use JitsiConferenceState::*; use JitsiConferenceState::*;
match locked_inner.state { let state = self.inner.lock().await.state;
match state {
Discovering => { Discovering => {
let iq = Iq::try_from(element)?; let iq = Iq::try_from(element)?;
if let IqType::Result(Some(element)) = iq.payload { if let IqType::Result(Some(element)) = iq.payload {
@ -377,34 +445,40 @@ impl StanzaFilter for JitsiConference {
let jitsi_disco_hash = let jitsi_disco_hash =
ecaps2::hash_ecaps2(&ecaps2::compute_disco(&jitsi_disco_info)?, Algo::Sha_256)?; ecaps2::hash_ecaps2(&ecaps2::compute_disco(&jitsi_disco_info)?, Algo::Sha_256)?;
self let mut presence = vec![
.send_presence(vec![ Muc::new().into(),
Muc::new().into(), ECaps2::new(vec![jitsi_disco_hash]).into(),
ECaps2::new(vec![jitsi_disco_hash]).into(), Element::builder("stats-id", "").append("gst-meet").build(),
Element::builder("stats-id", "").append("gst-meet").build(), Element::builder("jitsi_participant_codecType", "")
Element::builder("jitsi_participant_codecType", "") .append(self.config.video_codec.as_str())
.append(self.config.video_codec.as_str()) .build(),
.build(), Element::builder("jitsi_participant_region", "")
Element::builder("jitsi_participant_region", "") .append(self.config.region.as_str())
.append(self.config.region.as_str()) .build(),
.build(), Element::builder("audiomuted", "").append("false").build(),
Element::builder("audiomuted", "").append("false").build(), Element::builder("videomuted", "").append("false").build(),
Element::builder("videomuted", "").append("false").build(), Element::builder("nick", "http://jabber.org/protocol/nick")
Element::builder("nick", "http://jabber.org/protocol/nick") .append(self.config.nick.as_str())
.append(self.config.nick.as_str()) .build(),
.build(), Element::builder("region", "http://jitsi.org/jitsi-meet")
Element::builder("region", "http://jitsi.org/jitsi-meet") .attr("id", &self.config.region)
.attr("id", &self.config.region) .build(),
.build(), ];
]) presence.extend(
.await?; self
locked_inner.state = JoiningMuc; .config
.extra_muc_features
.iter()
.map(|feature| Element::builder("feature", "").attr("var", feature).build()),
);
self.send_presence(presence).await?;
self.inner.lock().await.state = JoiningMuc;
}, },
JoiningMuc => { JoiningMuc => {
let presence = Presence::try_from(element)?; let presence = Presence::try_from(element)?;
if BareJid::from(presence.from.as_ref().unwrap().clone()) == self.config.muc { if BareJid::from(presence.from.as_ref().unwrap().clone()) == self.config.muc {
debug!("Joined MUC: {}", self.config.muc); debug!("Joined MUC: {}", self.config.muc);
locked_inner.state = Idle; self.inner.lock().await.state = Idle;
} }
}, },
Idle => { Idle => {
@ -438,7 +512,7 @@ impl StanzaFilter for JitsiConference {
.with_from(Jid::Full(self.jid.clone())); .with_from(Jid::Full(self.jid.clone()));
self.xmpp_tx.send(result_iq.into()).await?; self.xmpp_tx.send(result_iq.into()).await?;
locked_inner.jingle_session = *self.jingle_session.lock().await =
Some(JingleSession::initiate(self, jingle).await?); Some(JingleSession::initiate(self, jingle).await?);
} }
else { else {
@ -453,8 +527,10 @@ impl StanzaFilter for JitsiConference {
.with_from(Jid::Full(self.jid.clone())); .with_from(Jid::Full(self.jid.clone()));
self.xmpp_tx.send(result_iq.into()).await?; self.xmpp_tx.send(result_iq.into()).await?;
locked_inner self
.jingle_session .jingle_session
.lock()
.await
.as_mut() .as_mut()
.context("not connected (no jingle session")? .context("not connected (no jingle session")?
.source_add(jingle) .source_add(jingle)
@ -470,11 +546,11 @@ impl StanzaFilter for JitsiConference {
} }
}, },
IqType::Result(_) => { IqType::Result(_) => {
if let Some(jingle_session) = locked_inner.jingle_session.as_ref() { if let Some(jingle_session) = self.jingle_session.lock().await.as_mut() {
if Some(iq.id) == jingle_session.accept_iq_id { if Some(iq.id) == jingle_session.accept_iq_id {
let colibri_url = jingle_session.colibri_url.clone(); let colibri_url = jingle_session.colibri_url.clone();
locked_inner.jingle_session.as_mut().unwrap().accept_iq_id = None; jingle_session.accept_iq_id = None;
debug!("Focus acknowledged session-accept"); debug!("Focus acknowledged session-accept");
@ -483,11 +559,7 @@ impl StanzaFilter for JitsiConference {
let colibri_channel = ColibriChannel::new(&colibri_url).await?; let colibri_channel = ColibriChannel::new(&colibri_url).await?;
let (tx, rx) = mpsc::channel(8); let (tx, rx) = mpsc::channel(8);
colibri_channel.subscribe(tx).await; colibri_channel.subscribe(tx).await;
locked_inner jingle_session.colibri_channel = Some(colibri_channel);
.jingle_session
.as_mut()
.unwrap()
.colibri_channel = Some(colibri_channel);
let self_ = self.clone(); let self_ = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -503,7 +575,7 @@ impl StanzaFilter for JitsiConference {
}); });
} }
if let Some(connected_tx) = locked_inner.connected_tx.take() { if let Some(connected_tx) = self.inner.lock().await.connected_tx.take() {
connected_tx.send(()).unwrap(); connected_tx.send(()).unwrap();
} }
} }
@ -522,47 +594,77 @@ impl StanzaFilter for JitsiConference {
let bare_from: BareJid = from.clone().into(); let bare_from: BareJid = from.clone().into();
if bare_from == self.config.muc && from.resource != "focus" { if bare_from == self.config.muc && from.resource != "focus" {
trace!("received MUC presence from {}", from.resource); trace!("received MUC presence from {}", from.resource);
for payload in presence.payloads { let nick_payload = presence
if !payload.is("x", ns::MUC_USER) { .payloads
continue; .iter()
} .find(|e| e.is("nick", ns::NICK))
let muc_user = MucUser::try_from(payload)?; .map(|e| Nick::try_from(e.clone()))
debug!("MUC user presence: {:?}", muc_user); .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 { for item in muc_user.items {
if let Some(jid) = &item.jid { if let Some(jid) = &item.jid {
if jid == &self.jid { if jid == &self.jid {
continue; continue;
} }
let participant = Participant { let participant = Participant {
jid: jid.clone(), jid: Some(jid.clone()),
muc_jid: from.clone(), muc_jid: from.clone(),
nick: item.nick, nick: item
.nick
.or_else(|| nick_payload.as_ref().map(|nick| nick.0.clone())),
}; };
if presence.type_ == presence::Type::Unavailable if presence.type_ == presence::Type::Unavailable
&& locked_inner && self
.inner
.lock()
.await
.participants .participants
.remove(&from.resource.clone()) .remove(&from.resource.clone())
.is_some() .is_some()
{ {
debug!("participant left: {:?}", jid); debug!("participant left: {:?}", jid);
if let Some(f) = &locked_inner.on_participant_left { if let Some(f) = &self
.inner
.lock()
.await
.on_participant_left
.as_ref()
.cloned()
{
debug!("calling on_participant_left with old participant"); debug!("calling on_participant_left with old participant");
if let Err(e) = f(self.clone(), participant).await { if let Err(e) = f(self.clone(), participant).await {
warn!("on_participant_left failed: {:?}", e); warn!("on_participant_left failed: {:?}", e);
} }
} }
} }
else if locked_inner else if self
.inner
.lock()
.await
.participants .participants
.insert(from.resource.clone(), participant.clone()) .insert(from.resource.clone(), participant.clone())
.is_none() .is_none()
{ {
debug!("new participant: {:?}", jid); debug!("new participant: {:?}", jid);
if let Some(f) = &locked_inner.on_participant { if let Some(f) = &self.inner.lock().await.on_participant.as_ref().cloned() {
debug!("calling on_participant with new participant"); debug!("calling on_participant with new participant");
if let Err(e) = f(self.clone(), participant).await { if let Err(e) = f(self.clone(), participant.clone()).await {
warn!("on_participant failed: {:?}", e); 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),
);
}
}
} }
} }
} }

View File

@ -236,6 +236,7 @@ impl JingleSession {
.transpose()?; .transpose()?;
} }
else { else {
debug!("skipping media: {}", description.media);
continue; continue;
} }
@ -246,20 +247,24 @@ impl JingleSession {
.context("missing ssrc-info")? .context("missing ssrc-info")?
.owner .owner
.clone(); .clone();
if owner == "jvb" {
debug!("skipping ssrc (owner = jvb)");
continue;
}
debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc);
remote_ssrc_map.insert( remote_ssrc_map.insert(
ssrc.id.parse()?, ssrc.id.parse()?,
Source { Source {
ssrc: ssrc.id.parse()?, ssrc: ssrc.id.parse()?,
participant_id: owner participant_id: if owner == "jvb" {
.split('/') None
.nth(1) }
.context("invalid ssrc-info owner")? else {
.to_owned(), Some(
owner
.split('/')
.nth(1)
.context("invalid ssrc-info owner")?
.to_owned(),
)
},
media_type: if description.media == "audio" { media_type: if description.media == "audio" {
MediaType::Audio MediaType::Audio
} }
@ -576,10 +581,10 @@ impl JingleSession {
})?; })?;
let handle = Handle::current(); let handle = Handle::current();
let inner_ = conference.inner.clone(); let jingle_session = conference.jingle_session.clone();
rtpbin.connect("new-jitterbuffer", false, move |values| { rtpbin.connect("new-jitterbuffer", false, move |values| {
let handle = handle.clone(); let handle = handle.clone();
let inner_ = inner_.clone(); let jingle_session = jingle_session.clone();
let f = move || { let f = move || {
let rtpjitterbuffer: gstreamer::Element = values[1].get()?; let rtpjitterbuffer: gstreamer::Element = values[1].get()?;
let session: u32 = values[2].get()?; let session: u32 = values[2].get()?;
@ -590,13 +595,12 @@ impl JingleSession {
); );
let source = handle.block_on(async move { let source = handle.block_on(async move {
let locked_inner = inner_.lock().await;
let jingle_session = locked_inner
.jingle_session
.as_ref()
.context("not connected (no jingle session)")?;
Ok::<_, anyhow::Error>( Ok::<_, anyhow::Error>(
jingle_session jingle_session
.lock()
.await
.as_ref()
.context("not connected (no jingle session)")?
.remote_ssrc_map .remote_ssrc_map
.get(&ssrc) .get(&ssrc)
.context(format!("unknown ssrc: {}", ssrc))? .context(format!("unknown ssrc: {}", ssrc))?
@ -604,7 +608,7 @@ impl JingleSession {
) )
})?; })?;
debug!("jitterbuffer is for remote source: {:?}", source); debug!("jitterbuffer is for remote source: {:?}", source);
if source.media_type == MediaType::Video { if source.media_type == MediaType::Video && source.participant_id.is_some() {
debug!("enabling RTX for ssrc {}", ssrc); debug!("enabling RTX for ssrc {}", ssrc);
rtpjitterbuffer.set_property("do-retransmission", true)?; rtpjitterbuffer.set_property("do-retransmission", true)?;
} }
@ -703,7 +707,7 @@ impl JingleSession {
{ {
let handle = Handle::current(); let handle = Handle::current();
let inner = conference.inner.clone(); let conference = conference.clone();
let pipeline = pipeline.clone(); let pipeline = pipeline.clone();
let rtpbin_ = rtpbin.clone(); let rtpbin_ = rtpbin.clone();
rtpbin.connect("pad-added", false, move |values| { rtpbin.connect("pad-added", false, move |values| {
@ -717,13 +721,13 @@ impl JingleSession {
let ssrc: u32 = parts.next().context("malformed pad name")?.parse()?; let ssrc: u32 = parts.next().context("malformed pad name")?.parse()?;
let pt: u8 = parts.next().context("malformed pad name")?.parse()?; let pt: u8 = parts.next().context("malformed pad name")?.parse()?;
let source = handle.block_on(async { let source = handle.block_on(async {
let locked_inner = inner.lock().await;
let jingle_session = locked_inner
.jingle_session
.as_ref()
.context("not connected (no jingle session)")?;
Ok::<_, anyhow::Error>( Ok::<_, anyhow::Error>(
jingle_session conference
.jingle_session
.lock()
.await
.as_ref()
.context("not connected (no jingle session)")?
.remote_ssrc_map .remote_ssrc_map
.get(&ssrc) .get(&ssrc)
.context(format!("unknown ssrc: {}", ssrc))? .context(format!("unknown ssrc: {}", ssrc))?
@ -805,26 +809,32 @@ impl JingleSession {
.static_pad("src") .static_pad("src")
.context("depayloader has no src pad")?; .context("depayloader has no src pad")?;
if let Some(participant_bin) = if let Some(participant_id) = source.participant_id {
pipeline.by_name(&format!("participant_{}", source.participant_id)) handle.block_on(conference.ensure_participant(&participant_id))?;
{ if let Some(participant_bin) =
let sink_pad_name = match source.media_type { pipeline.by_name(&format!("participant_{}", participant_id))
MediaType::Audio => "audio", {
MediaType::Video => "video", let sink_pad_name = match source.media_type {
}; MediaType::Audio => "audio",
if let Some(sink_pad) = participant_bin.static_pad(sink_pad_name) { MediaType::Video => "video",
debug!("linking depayloader to participant bin"); };
src_pad.link(&sink_pad)?; if let Some(sink_pad) = participant_bin.static_pad(sink_pad_name) {
debug!("linking depayloader to participant bin");
src_pad.link(&sink_pad)?;
}
else {
warn!(
"no {} sink pad in {} participant bin",
sink_pad_name, participant_id
);
}
} }
else { else {
warn!( debug!("no participant bin for {}", participant_id);
"no {} sink pad in {} participant bin",
sink_pad_name, source.participant_id
);
} }
} }
else { else {
debug!("no participant bin for {}", source.participant_id); debug!("not looking for participant bin, source is owned by JVB");
} }
if !src_pad.is_linked() { if !src_pad.is_linked() {
@ -861,38 +871,43 @@ impl JingleSession {
)?; )?;
audio_sink_element.set_property("min-ptime", 10i64 * 1000 * 1000)?; audio_sink_element.set_property("min-ptime", 10i64 * 1000 * 1000)?;
audio_sink_element.set_property("ssrc", audio_ssrc)?; audio_sink_element.set_property("ssrc", audio_ssrc)?;
audio_sink_element.set_property("auto-header-extension", false)?; if audio_sink_element.has_property("auto-header-extension", None) {
audio_sink_element.connect("request-extension", false, move |values| { audio_sink_element.set_property("auto-header-extension", false)?;
let f = || { audio_sink_element.connect("request-extension", false, move |values| {
let ext_id: u32 = values[1].get()?; let f = || {
let ext_uri: String = values[2].get()?; let ext_id: u32 = values[1].get()?;
debug!( let ext_uri: String = values[2].get()?;
"audio payloader requested extension: {} {}", debug!(
ext_id, ext_uri "audio payloader requested extension: {} {}",
); ext_id, ext_uri
let hdrext = );
RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?; let hdrext =
hdrext.set_id(ext_id); RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?;
if ext_uri == RTP_HDREXT_ABS_SEND_TIME { hdrext.set_id(ext_id);
if ext_uri == RTP_HDREXT_ABS_SEND_TIME {
}
else if ext_uri == RTP_HDREXT_SSRC_AUDIO_LEVEL {
}
else if ext_uri == RTP_HDREXT_TRANSPORT_CC {
// hdrext.set_property("n-streams", 2u32)?;
}
else {
bail!("unknown rtp hdrext: {}", ext_uri);
}
Ok::<_, anyhow::Error>(hdrext)
};
match f() {
Ok(hdrext) => Some(hdrext.to_value()),
Err(e) => {
warn!("request-extension: {:?}", e);
None
},
} }
else if ext_uri == RTP_HDREXT_SSRC_AUDIO_LEVEL { })?;
} }
else if ext_uri == RTP_HDREXT_TRANSPORT_CC { else {
// hdrext.set_property("n-streams", 2u32)?; debug!("audio payloader: no rtp header extension support");
} }
else {
bail!("unknown rtp hdrext: {}", ext_uri);
}
Ok::<_, anyhow::Error>(hdrext)
};
match f() {
Ok(hdrext) => Some(hdrext.to_value()),
Err(e) => {
warn!("request-extension: {:?}", e);
None
},
}
})?;
pipeline.add(&audio_sink_element)?; pipeline.add(&audio_sink_element)?;
let video_sink_element = match conference.config.video_codec.as_str() { let video_sink_element = match conference.config.video_codec.as_str() {
@ -926,43 +941,47 @@ impl JingleSession {
other => bail!("unsupported video codec: {}", other), other => bail!("unsupported video codec: {}", other),
}; };
video_sink_element.set_property("ssrc", video_ssrc)?; video_sink_element.set_property("ssrc", video_ssrc)?;
video_sink_element.set_property("auto-header-extension", false)?; if video_sink_element.has_property("auto-header-extension", None) {
video_sink_element.connect("request-extension", false, move |values| { video_sink_element.set_property("auto-header-extension", false)?;
let f = || { video_sink_element.connect("request-extension", false, move |values| {
let ext_id: u32 = values[1].get()?; let f = || {
let ext_uri: String = values[2].get()?; let ext_id: u32 = values[1].get()?;
debug!( let ext_uri: String = values[2].get()?;
"video payloader requested extension: {} {}", debug!(
ext_id, ext_uri "video payloader requested extension: {} {}",
); ext_id, ext_uri
let hdrext = );
RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?; let hdrext =
hdrext.set_id(ext_id); RTPHeaderExtension::create_from_uri(&ext_uri).context("failed to create hdrext")?;
if ext_uri == RTP_HDREXT_ABS_SEND_TIME { hdrext.set_id(ext_id);
if ext_uri == RTP_HDREXT_ABS_SEND_TIME {
}
else if ext_uri == RTP_HDREXT_TRANSPORT_CC {
// hdrext.set_property("n-streams", 2u32)?;
}
else {
bail!("unknown rtp hdrext: {}", ext_uri);
}
Ok::<_, anyhow::Error>(hdrext)
};
match f() {
Ok(hdrext) => Some(hdrext.to_value()),
Err(e) => {
warn!("request-extension: {:?}", e);
None
},
} }
else if ext_uri == RTP_HDREXT_TRANSPORT_CC { })?;
// hdrext.set_property("n-streams", 2u32)?; }
} else {
else { debug!("video payloader: no rtp header extension support");
bail!("unknown rtp hdrext: {}", ext_uri); }
}
Ok::<_, anyhow::Error>(hdrext)
};
match f() {
Ok(hdrext) => Some(hdrext.to_value()),
Err(e) => {
warn!("request-extension: {:?}", e);
None
},
}
})?;
pipeline.add(&video_sink_element)?; pipeline.add(&video_sink_element)?;
let mut audio_caps = gstreamer::Caps::builder("application/x-rtp"); let mut audio_caps = gstreamer::Caps::builder("application/x-rtp");
// TODO: fails to negotiate if let Some(hdrext) = audio_hdrext_ssrc_audio_level {
// if let Some(hdrext) = audio_hdrext_ssrc_audio_level { audio_caps = audio_caps.field(&format!("extmap-{}", hdrext), RTP_HDREXT_SSRC_AUDIO_LEVEL);
// audio_caps = audio_caps.field(&format!("extmap-{}", hdrext), RTP_HDREXT_SSRC_AUDIO_LEVEL); }
// }
if let Some(hdrext) = audio_hdrext_transport_cc { if let Some(hdrext) = audio_hdrext_transport_cc {
audio_caps = audio_caps.field(&format!("extmap-{}", hdrext), RTP_HDREXT_TRANSPORT_CC); audio_caps = audio_caps.field(&format!("extmap-{}", hdrext), RTP_HDREXT_TRANSPORT_CC);
} }
@ -981,13 +1000,19 @@ impl JingleSession {
video_capsfilter.set_property("caps", video_caps.build())?; video_capsfilter.set_property("caps", video_caps.build())?;
pipeline.add(&video_capsfilter)?; pipeline.add(&video_capsfilter)?;
debug!("linking video payloader -> rtpbin"); let rtpfunnel = gstreamer::ElementFactory::make("rtpfunnel", None)?;
video_sink_element.link(&video_capsfilter)?; pipeline.add(&rtpfunnel)?;
video_capsfilter.link_pads(None, &rtpbin, Some("send_rtp_sink_0"))?;
debug!("linking audio payloader -> rtpbin"); debug!("linking video payloader -> rtpfunnel");
video_sink_element.link(&video_capsfilter)?;
video_capsfilter.link(&rtpfunnel)?;
debug!("linking audio payloader -> rtpfunnel");
audio_sink_element.link(&audio_capsfilter)?; audio_sink_element.link(&audio_capsfilter)?;
audio_capsfilter.link_pads(None, &rtpbin, Some("send_rtp_sink_1"))?; audio_capsfilter.link(&rtpfunnel)?;
debug!("linking rtpfunnel -> rtpbin");
rtpfunnel.link_pads(None, &rtpbin, Some("send_rtp_sink_0"))?;
debug!("link dtlssrtpdec -> rtpbin"); debug!("link dtlssrtpdec -> rtpbin");
dtlssrtpdec.link_pads(Some("rtp_src"), &rtpbin, Some("recv_rtp_sink_0"))?; dtlssrtpdec.link_pads(Some("rtp_src"), &rtpbin, Some("recv_rtp_sink_0"))?;
@ -996,8 +1021,6 @@ impl JingleSession {
debug!("linking rtpbin -> dtlssrtpenc"); debug!("linking rtpbin -> dtlssrtpenc");
rtpbin.link_pads(Some("send_rtp_src_0"), &dtlssrtpenc, Some("rtp_sink_0"))?; rtpbin.link_pads(Some("send_rtp_src_0"), &dtlssrtpenc, Some("rtp_sink_0"))?;
rtpbin.link_pads(Some("send_rtcp_src_0"), &dtlssrtpenc, Some("rtcp_sink_0"))?; rtpbin.link_pads(Some("send_rtcp_src_0"), &dtlssrtpenc, Some("rtcp_sink_0"))?;
rtpbin.link_pads(Some("send_rtp_src_1"), &dtlssrtpenc, Some("rtp_sink_1"))?;
rtpbin.link_pads(Some("send_rtcp_src_1"), &dtlssrtpenc, Some("rtcp_sink_1"))?;
debug!("linking ice src -> dtlssrtpdec"); debug!("linking ice src -> dtlssrtpdec");
nicesrc.link(&dtlssrtpdec)?; nicesrc.link(&dtlssrtpdec)?;
@ -1177,15 +1200,12 @@ impl JingleSession {
}; };
if initiate_content.name.0 == "audio" { if initiate_content.name.0 == "audio" {
// TODO: fails to negotiate if let Some(hdrext) = audio_hdrext_ssrc_audio_level {
// if let Some(hdrext) = audio_hdrext_ssrc_audio_level { description.hdrexts.push(RtpHdrext::new(
// if audio_hdrext_supported { hdrext.to_string(),
// description.hdrexts.push(RtpHdrext::new(hdrext.to_string(), RTP_HDREXT_SSRC_AUDIO_LEVEL.to_owned())); RTP_HDREXT_SSRC_AUDIO_LEVEL.to_owned(),
// } ));
// else { }
// debug!("ssrc-audio-level hdrext requested but not supported");
// }
// }
if let Some(hdrext) = audio_hdrext_transport_cc { if let Some(hdrext) = audio_hdrext_transport_cc {
description.hdrexts.push(RtpHdrext::new( description.hdrexts.push(RtpHdrext::new(
hdrext.to_string(), hdrext.to_string(),
@ -1276,21 +1296,24 @@ impl JingleSession {
.context("missing ssrc-info")? .context("missing ssrc-info")?
.owner .owner
.clone(); .clone();
if owner == "jvb" {
debug!("skipping ssrc (owner = jvb)");
continue;
}
debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc); debug!("adding ssrc to remote_ssrc_map: {:?}", ssrc);
self.remote_ssrc_map.insert( self.remote_ssrc_map.insert(
ssrc.id.parse()?, ssrc.id.parse()?,
Source { Source {
ssrc: ssrc.id.parse()?, ssrc: ssrc.id.parse()?,
participant_id: owner participant_id: if owner == "jvb" {
.split('/') None
.nth(1) }
.context("invalid ssrc-info owner")? else {
.to_owned(), Some(
owner
.split('/')
.nth(1)
.context("invalid ssrc-info owner")?
.to_owned(),
)
},
media_type: if description.media == "audio" { media_type: if description.media == "audio" {
MediaType::Audio MediaType::Audio
} }

View File

@ -1,6 +1,5 @@
pub mod colibri; mod colibri;
mod conference; mod conference;
mod connection;
mod jingle; mod jingle;
mod pinger; mod pinger;
mod source; mod source;
@ -10,9 +9,10 @@ mod xmpp;
pub use crate::{ pub use crate::{
colibri::ColibriMessage, colibri::ColibriMessage,
conference::{JitsiConference, JitsiConferenceConfig, Participant}, conference::{Feature, JitsiConference, JitsiConferenceConfig, Participant},
connection::JitsiConnection,
source::MediaType, source::MediaType,
stanza_filter::StanzaFilter,
xmpp::connection::{Authentication, Connection},
}; };
#[cfg(feature = "tracing-subscriber")] #[cfg(feature = "tracing-subscriber")]

View File

@ -1,7 +1,7 @@
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Source { pub(crate) struct Source {
pub(crate) ssrc: u32, pub(crate) ssrc: u32,
pub(crate) participant_id: String, pub(crate) participant_id: Option<String>,
pub(crate) media_type: MediaType, pub(crate) media_type: MediaType,
} }

View File

@ -3,7 +3,7 @@ use async_trait::async_trait;
use xmpp_parsers::Element; use xmpp_parsers::Element;
#[async_trait] #[async_trait]
pub(crate) trait StanzaFilter { pub trait StanzaFilter {
fn filter(&self, element: &Element) -> bool; fn filter(&self, element: &Element) -> bool;
async fn take(&self, element: Element) -> Result<()>; async fn take(&self, element: Element) -> Result<()>;
} }

View File

@ -23,16 +23,10 @@ use xmpp_parsers::{
BareJid, Element, FullJid, Jid, BareJid, Element, FullJid, Jid,
}; };
use crate::{ use crate::{pinger::Pinger, stanza_filter::StanzaFilter, util::generate_id, xmpp};
conference::{JitsiConference, JitsiConferenceConfig},
pinger::Pinger,
stanza_filter::StanzaFilter,
util::generate_id,
xmpp,
};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum JitsiConnectionState { enum ConnectionState {
OpeningPreAuthentication, OpeningPreAuthentication,
ReceivingFeaturesPreAuthentication, ReceivingFeaturesPreAuthentication,
Authenticating, Authenticating,
@ -44,35 +38,41 @@ enum JitsiConnectionState {
Idle, Idle,
} }
struct JitsiConnectionInner { struct ConnectionInner {
state: JitsiConnectionState, state: ConnectionState,
xmpp_domain: BareJid,
jid: Option<FullJid>, jid: Option<FullJid>,
xmpp_domain: BareJid,
authentication: Authentication,
external_services: Vec<xmpp::extdisco::Service>, external_services: Vec<xmpp::extdisco::Service>,
connected_tx: Option<oneshot::Sender<Result<()>>>, connected_tx: Option<oneshot::Sender<Result<()>>>,
stanza_filters: Vec<Box<dyn StanzaFilter + Send + Sync>>, stanza_filters: Vec<Box<dyn StanzaFilter + Send + Sync>>,
} }
impl fmt::Debug for JitsiConnectionInner { impl fmt::Debug for ConnectionInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JitsiConnectionInner") f.debug_struct("ConnectionInner")
.field("state", &self.state) .field("state", &self.state)
.field("xmpp_domain", &self.xmpp_domain)
.field("jid", &self.jid) .field("jid", &self.jid)
.finish() .finish()
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct JitsiConnection { pub struct Connection {
tx: mpsc::Sender<Element>, pub(crate) tx: mpsc::Sender<Element>,
inner: Arc<Mutex<JitsiConnectionInner>>, inner: Arc<Mutex<ConnectionInner>>,
} }
impl JitsiConnection { pub enum Authentication {
Anonymous,
Plain { username: String, password: String },
}
impl Connection {
pub async fn new( pub async fn new(
websocket_url: &str, websocket_url: &str,
xmpp_domain: &str, xmpp_domain: &str,
authentication: Authentication,
) -> Result<(Self, impl Future<Output = ()>)> { ) -> Result<(Self, impl Future<Output = ()>)> {
let websocket_url: Uri = websocket_url.parse().context("invalid WebSocket URL")?; let websocket_url: Uri = websocket_url.parse().context("invalid WebSocket URL")?;
let xmpp_domain: BareJid = xmpp_domain.parse().context("invalid XMPP domain")?; let xmpp_domain: BareJid = xmpp_domain.parse().context("invalid XMPP domain")?;
@ -88,10 +88,11 @@ impl JitsiConnection {
let (sink, stream) = websocket.split(); let (sink, stream) = websocket.split();
let (tx, rx) = mpsc::channel(64); let (tx, rx) = mpsc::channel(64);
let inner = Arc::new(Mutex::new(JitsiConnectionInner { let inner = Arc::new(Mutex::new(ConnectionInner {
state: JitsiConnectionState::OpeningPreAuthentication, state: ConnectionState::OpeningPreAuthentication,
xmpp_domain,
jid: None, jid: None,
xmpp_domain,
authentication,
external_services: vec![], external_services: vec![],
connected_tx: None, connected_tx: None,
stanza_filters: vec![], stanza_filters: vec![],
@ -102,8 +103,8 @@ impl JitsiConnection {
inner: inner.clone(), inner: inner.clone(),
}; };
let writer = JitsiConnection::write_loop(rx, sink); let writer = Connection::write_loop(rx, sink);
let reader = JitsiConnection::read_loop(inner, tx, stream); let reader = Connection::read_loop(inner, tx, stream);
let background = async move { let background = async move {
tokio::select! { tokio::select! {
@ -115,6 +116,11 @@ impl JitsiConnection {
Ok((connection, background)) Ok((connection, background))
} }
pub async fn add_stanza_filter(&self, stanza_filter: impl StanzaFilter + Send + Sync + 'static) {
let mut locked_inner = self.inner.lock().await;
locked_inner.stanza_filters.push(Box::new(stanza_filter));
}
pub async fn connect(&self) -> Result<()> { pub async fn connect(&self) -> Result<()> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
@ -128,48 +134,14 @@ impl JitsiConnection {
rx.await? rx.await?
} }
pub async fn join_conference( pub async fn jid(&self) -> Option<FullJid> {
&self, let mut locked_inner = self.inner.lock().await;
glib_main_context: glib::MainContext, locked_inner.jid.clone()
config: JitsiConferenceConfig, }
) -> Result<JitsiConference> {
let conference_stanza = xmpp::jitsi::Conference {
machine_uid: Uuid::new_v4().to_string(),
room: config.muc.to_string(),
properties: hashmap! {
// Disable voice processing
"stereo".to_string() => "true".to_string(),
"startBitrate".to_string() => "800".to_string(),
},
};
let iq = pub async fn external_services(&self) -> Vec<xmpp::extdisco::Service> {
Iq::from_set(generate_id(), conference_stanza).with_to(Jid::Full(config.focus.clone())); let mut locked_inner = self.inner.lock().await;
self.tx.send(iq.into()).await?; locked_inner.external_services.clone()
let conference = {
let mut locked_inner = self.inner.lock().await;
let conference = JitsiConference::new(
glib_main_context,
locked_inner
.jid
.as_ref()
.context("not connected (no jid)")?
.clone(),
self.tx.clone(),
config,
locked_inner.external_services.clone(),
)
.await?;
locked_inner
.stanza_filters
.push(Box::new(conference.clone()));
conference
};
conference.connected().await?;
Ok(conference)
} }
async fn write_loop<S>(rx: mpsc::Receiver<Element>, mut sink: S) -> Result<()> async fn write_loop<S>(rx: mpsc::Receiver<Element>, mut sink: S) -> Result<()>
@ -189,7 +161,7 @@ impl JitsiConnection {
} }
async fn read_loop<S>( async fn read_loop<S>(
inner: Arc<Mutex<JitsiConnectionInner>>, inner: Arc<Mutex<ConnectionInner>>,
tx: mpsc::Sender<Element>, tx: mpsc::Sender<Element>,
mut stream: S, mut stream: S,
) -> Result<()> ) -> Result<()>
@ -217,7 +189,7 @@ impl JitsiConnection {
let mut locked_inner = inner.lock().await; let mut locked_inner = inner.lock().await;
use JitsiConnectionState::*; use ConnectionState::*;
match locked_inner.state { match locked_inner.state {
OpeningPreAuthentication => { OpeningPreAuthentication => {
Open::try_from(element)?; Open::try_from(element)?;
@ -225,9 +197,22 @@ impl JitsiConnection {
locked_inner.state = ReceivingFeaturesPreAuthentication; locked_inner.state = ReceivingFeaturesPreAuthentication;
}, },
ReceivingFeaturesPreAuthentication => { ReceivingFeaturesPreAuthentication => {
let auth = Auth { let auth = match &locked_inner.authentication {
mechanism: Mechanism::Anonymous, Authentication::Anonymous => Auth {
data: vec![], mechanism: Mechanism::Anonymous,
data: vec![],
},
Authentication::Plain { username, password } => {
let mut data = Vec::with_capacity(username.len() + password.len() + 2);
data.push(0u8);
data.extend_from_slice(username.as_bytes());
data.push(0u8);
data.extend_from_slice(password.as_bytes());
Auth {
mechanism: Mechanism::Plain,
data,
}
},
}; };
tx.send(auth.into()).await?; tx.send(auth.into()).await?;
locked_inner.state = Authenticating; locked_inner.state = Authenticating;
@ -241,8 +226,10 @@ impl JitsiConnection {
}, },
OpeningPostAuthentication => { OpeningPostAuthentication => {
Open::try_from(element)?; Open::try_from(element)?;
info!("Logged in anonymously"); match &locked_inner.authentication {
Authentication::Anonymous => info!("Logged in anonymously"),
Authentication::Plain { .. } => info!("Logged in with PLAIN"),
}
locked_inner.state = ReceivingFeaturesPostAuthentication; locked_inner.state = ReceivingFeaturesPostAuthentication;
}, },
ReceivingFeaturesPostAuthentication => { ReceivingFeaturesPostAuthentication => {
@ -250,28 +237,33 @@ impl JitsiConnection {
tx.send(iq.into()).await?; tx.send(iq.into()).await?;
locked_inner.state = Binding; locked_inner.state = Binding;
}, },
Binding => { Binding => match Iq::try_from(element) {
let iq = Iq::try_from(element)?; Ok(iq) => {
let jid = if let IqType::Result(Some(element)) = iq.payload { let jid = if let IqType::Result(Some(element)) = iq.payload {
let bind = BindResponse::try_from(element)?; let bind = BindResponse::try_from(element)?;
FullJid::try_from(bind)? FullJid::try_from(bind)?
} }
else { else {
bail!("bind failed"); bail!("bind failed");
}; };
info!("My JID: {}", jid); info!("My JID: {}", jid);
locked_inner.jid = Some(jid.clone()); locked_inner.jid = Some(jid.clone());
locked_inner.stanza_filters.push(Box::new(Pinger { locked_inner.stanza_filters.push(Box::new(Pinger {
jid: jid.clone(), jid: jid.clone(),
tx: tx.clone(), tx: tx.clone(),
})); }));
let iq = Iq::from_get(generate_id(), DiscoInfoQuery { node: None }) let iq = Iq::from_get(generate_id(), DiscoInfoQuery { node: None })
.with_from(Jid::Full(jid.clone())) .with_from(Jid::Full(jid.clone()))
.with_to(Jid::Bare(locked_inner.xmpp_domain.clone())); .with_to(Jid::Bare(locked_inner.xmpp_domain.clone()));
tx.send(iq.into()).await?; tx.send(iq.into()).await?;
locked_inner.state = Discovering; locked_inner.state = Discovering;
},
Err(e) => debug!(
"received unexpected element while waiting for bind response: {}",
e
),
}, },
Discovering => { Discovering => {
let iq = Iq::try_from(element)?; let iq = Iq::try_from(element)?;

View File

@ -26,7 +26,7 @@ impl From<ServicesQuery> for Element {
impl IqGetPayload for ServicesQuery {} impl IqGetPayload for ServicesQuery {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Service { pub struct Service {
pub(crate) r#type: String, pub(crate) r#type: String,
pub(crate) name: Option<String>, pub(crate) name: Option<String>,
pub(crate) host: String, pub(crate) host: String,

View File

@ -37,3 +37,19 @@ impl From<Conference> for Element {
builder.build() builder.build()
} }
} }
pub(crate) struct JsonMessage {
pub(crate) payload: serde_json::Value,
}
impl TryFrom<JsonMessage> for Element {
type Error = anyhow::Error;
fn try_from(message: JsonMessage) -> Result<Element> {
Ok(
Element::builder("json-message", ns::JITSI_JITMEET)
.append(serde_json::to_string(&message.payload)?)
.build(),
)
}
}

View File

@ -1,3 +1,4 @@
pub mod connection;
pub(crate) mod extdisco; pub(crate) mod extdisco;
pub(crate) mod jitsi; pub(crate) mod jitsi;
mod ns; mod ns;

View File

@ -2,3 +2,5 @@
pub(crate) const EXTDISCO: &str = "urn:xmpp:extdisco:2"; pub(crate) const EXTDISCO: &str = "urn:xmpp:extdisco:2";
pub(crate) const JITSI_FOCUS: &str = "http://jitsi.org/protocol/focus"; pub(crate) const JITSI_FOCUS: &str = "http://jitsi.org/protocol/focus";
pub(crate) const JITSI_JITMEET: &str = "http://jitsi.org/jitmeet";