From 26d10d1dcb95f11b65611473c9840e13683cb5ec Mon Sep 17 00:00:00 2001
From: fiaxh <git@lightrise.org>
Date: Thu, 4 Nov 2021 17:33:08 +0100
Subject: Add multiparty call support to libdino and xmpp-vala

---
 libdino/src/entity/call.vala                 |  48 +-
 libdino/src/plugin/interfaces.vala           |   4 +-
 libdino/src/service/call_peer_state.vala     | 453 ++++++++++++++++++
 libdino/src/service/call_state.vala          | 302 ++++++++++++
 libdino/src/service/calls.vala               | 658 ++++++++-------------------
 libdino/src/service/connection_manager.vala  |   2 +
 libdino/src/service/content_item_store.vala  |   4 +-
 libdino/src/service/database.vala            |  32 +-
 libdino/src/service/module_manager.vala      |   4 +
 libdino/src/service/muc_manager.vala         |  43 +-
 libdino/src/service/notification_events.vala |   2 +-
 11 files changed, 1066 insertions(+), 486 deletions(-)
 create mode 100644 libdino/src/service/call_peer_state.vala
 create mode 100644 libdino/src/service/call_state.vala

(limited to 'libdino/src')

diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala
index 577b3ab8..a5ab672e 100644
--- a/libdino/src/entity/call.vala
+++ b/libdino/src/entity/call.vala
@@ -20,14 +20,9 @@ namespace Dino.Entities {
 
         public int id { get; set; default=-1; }
         public Account account { get; set; }
-        public Jid counterpart { get; set; }
+        public Jid counterpart { get; set; } // For backwards compatibility with db version 21. Not to be used anymore.
+        public Gee.List<Jid> counterparts = new Gee.ArrayList<Jid>(Jid.equals_bare_func);
         public Jid ourpart { get; set; }
-        public Jid? from {
-            get { return direction == DIRECTION_OUTGOING ? ourpart : counterpart; }
-        }
-        public Jid? to {
-            get { return direction == DIRECTION_OUTGOING ? counterpart : ourpart; }
-        }
         public bool direction { get; set; }
         public DateTime time { get; set; }
         public DateTime local_time { get; set; }
@@ -47,6 +42,7 @@ namespace Dino.Entities {
             counterpart = db.get_jid_by_id(row[db.call.counterpart_id]);
             string counterpart_resource = row[db.call.counterpart_resource];
             if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
+            counterparts.add(counterpart);
 
             string our_resource = row[db.call.our_resource];
             if (our_resource != null) {
@@ -61,6 +57,15 @@ namespace Dino.Entities {
             encryption = (Encryption) row[db.call.encryption];
             state = (State) row[db.call.state];
 
+            Qlite.QueryBuilder counterparts_select = db.call_counterpart.select().with(db.call_counterpart.call_id, "=", id);
+            foreach (Qlite.Row counterparts_row in counterparts_select) {
+                Jid peer = db.get_jid_by_id(counterparts_row[db.call_counterpart.jid_id]);
+                if (!counterparts.contains(peer)) { // Legacy: The first peer is also in the `call` table. Don't add twice.
+                    counterparts.add(peer);
+                }
+                if (counterpart == null) counterpart = peer;
+            }
+
             notify.connect(on_update);
         }
 
@@ -70,8 +75,6 @@ namespace Dino.Entities {
             this.db = db;
             Qlite.InsertBuilder builder = db.call.insert()
                     .value(db.call.account_id, account.id)
-                    .value(db.call.counterpart_id, db.get_jid_id(counterpart))
-                    .value(db.call.counterpart_resource, counterpart.resourcepart)
                     .value(db.call.our_resource, ourpart.resourcepart)
                     .value(db.call.direction, direction)
                     .value(db.call.time, (long) time.to_unix())
@@ -83,11 +86,38 @@ namespace Dino.Entities {
             } else {
                 builder.value(db.call.end_time, (long) local_time.to_unix());
             }
+            if (counterpart != null) {
+                builder.value(db.call.counterpart_id, db.get_jid_id(counterpart))
+                    .value(db.call.counterpart_resource, counterpart.resourcepart);
+            }
             id = (int) builder.perform();
 
+            foreach (Jid peer in counterparts) {
+                db.call_counterpart.insert()
+                        .value(db.call_counterpart.call_id, id)
+                        .value(db.call_counterpart.jid_id, db.get_jid_id(peer))
+                        .value(db.call_counterpart.resource, peer.resourcepart)
+                        .perform();
+            }
+
             notify.connect(on_update);
         }
 
+        public void add_peer(Jid peer) {
+            if (counterpart == null) counterpart = peer;
+
+            if (counterparts.contains(peer)) return;
+
+            counterparts.add(peer);
+            if (db != null) {
+                db.call_counterpart.insert()
+                        .value(db.call_counterpart.call_id, id)
+                        .value(db.call_counterpart.jid_id, db.get_jid_id(peer))
+                        .value(db.call_counterpart.resource, peer.resourcepart)
+                        .perform();
+            }
+        }
+
         public bool equals(Call c) {
             return equals_func(this, c);
         }
diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala
index eadbb085..fb80fef6 100644
--- a/libdino/src/plugin/interfaces.vala
+++ b/libdino/src/plugin/interfaces.vala
@@ -106,11 +106,13 @@ public abstract interface VideoCallPlugin : Object {
     public abstract MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream stream, bool incoming);
     public abstract void set_pause(Xmpp.Xep.JingleRtp.Stream stream, bool pause);
     public abstract void set_device(Xmpp.Xep.JingleRtp.Stream stream, MediaDevice? device);
+
+    public abstract void dump_dot();
 }
 
 public abstract interface VideoCallWidget : Object {
     public signal void resolution_changed(uint width, uint height);
-    public abstract void display_stream(Xmpp.Xep.JingleRtp.Stream stream); // TODO: Multi participant
+    public abstract void display_stream(Xmpp.Xep.JingleRtp.Stream stream, Jid jid);
     public abstract void display_device(MediaDevice device);
     public abstract void detach();
 }
diff --git a/libdino/src/service/call_peer_state.vala b/libdino/src/service/call_peer_state.vala
new file mode 100644
index 00000000..ddd0d8dd
--- /dev/null
+++ b/libdino/src/service/call_peer_state.vala
@@ -0,0 +1,453 @@
+using Dino.Entities;
+using Gee;
+using Xmpp;
+
+public class Dino.PeerState : Object {
+    public signal void counterpart_sends_video_updated(bool mute);
+    public signal void info_received(Xep.JingleRtp.CallSessionInfo session_info);
+
+    public signal void connection_ready();
+    public signal void session_terminated(bool we_terminated, string? reason_name, string? reason_text);
+    public signal void encryption_updated(Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption, bool same);
+
+    public StreamInteractor stream_interactor;
+    public Calls calls;
+    public Call call;
+    public Jid jid;
+    public Xep.Jingle.Session session;
+    public string sid;
+    public string internal_id = Xmpp.random_uuid();
+
+    public Xep.JingleRtp.Parameters? audio_content_parameter = null;
+    public Xep.JingleRtp.Parameters? video_content_parameter = null;
+    public Xep.Jingle.Content? audio_content = null;
+    public Xep.Jingle.Content? video_content = null;
+    public Xep.Jingle.ContentEncryption? video_encryption = null;
+    public Xep.Jingle.ContentEncryption? audio_encryption = null;
+    public bool encryption_keys_same = false;
+    public HashMap<string, Xep.Jingle.ContentEncryption>? video_encryptions = null;
+    public HashMap<string, Xep.Jingle.ContentEncryption>? audio_encryptions = null;
+
+    public bool first_peer = false;
+    public bool accepted_jmi = false;
+    public bool waiting_for_inbound_muji_connection = false;
+    public Xep.Muji.GroupCall? group_call { get; set; }
+
+    public bool counterpart_sends_video = false;
+    public bool we_should_send_audio { get; set; default=false; }
+    public bool we_should_send_video { get; set; default=false; }
+
+    public PeerState(Jid jid, Call call, StreamInteractor stream_interactor) {
+        this.jid = jid;
+        this.call = call;
+        this.stream_interactor = stream_interactor;
+        this.calls = stream_interactor.get_module(Calls.IDENTITY);
+
+        var session_info_type = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY).session_info_type;
+        session_info_type.mute_update_received.connect((session,mute, name) => {
+            if (this.sid != session.sid) return;
+
+            foreach (Xep.Jingle.Content content in session.contents) {
+                if (name == null || content.content_name == name) {
+                    Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
+                    if (rtp_content_parameter != null) {
+                        on_counterpart_mute_update(mute, rtp_content_parameter.media);
+                    }
+                }
+            }
+        });
+        session_info_type.info_received.connect((session, session_info) => {
+            if (this.sid != session.sid) return;
+
+            info_received(session_info);
+        });
+    }
+
+    public async void initiate_call(Jid counterpart) {
+        Gee.List<Jid> call_resources = yield calls.get_call_resources(call.account, counterpart);
+
+        bool do_jmi = false;
+        Jid? jid_for_direct = null;
+        if (yield calls.contains_jmi_resources(call.account, call_resources)) {
+            do_jmi = true;
+        } else if (!call_resources.is_empty) {
+            jid_for_direct = call_resources[0];
+        } else if (calls.has_jmi_resources(jid)) {
+            do_jmi = true;
+        }
+
+        sid = Xmpp.random_uuid();
+
+        if (do_jmi) {
+            XmppStream? stream = stream_interactor.get_stream(call.account);
+
+            calls.current_jmi_request_call[call.account] = calls.call_states[call];
+            calls.current_jmi_request_peer[call.account] = this;
+
+            var descriptions = new ArrayList<StanzaNode>();
+            descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
+            if (we_should_send_video) {
+                descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "video"));
+            }
+
+            stream.get_module(Xmpp.Xep.JingleMessageInitiation.Module.IDENTITY).send_session_propose_to_peer(stream, jid, sid, descriptions);
+        } else if (jid_for_direct != null) {
+            yield call_resource(jid_for_direct);
+        }
+    }
+
+    public async void call_resource(Jid full_jid) {
+        XmppStream? stream = stream_interactor.get_stream(call.account);
+        if (stream == null) return;
+
+        if (sid == null) sid = Xmpp.random_uuid();
+
+        Xep.Jingle.Session session = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).start_call(stream, full_jid, we_should_send_video, sid, group_call != null ? group_call.muc_jid : null);
+        set_session(session);
+    }
+
+    public void accept() {
+        if (session != null) {
+            foreach (Xep.Jingle.Content content in session.contents) {
+                content.accept();
+            }
+        } else {
+            // Only a JMI so far
+            XmppStream stream = stream_interactor.get_stream(call.account);
+            if (stream == null) return;
+
+            accepted_jmi = true;
+
+            calls.current_jmi_request_call[call.account] = calls.call_states[call];
+            calls.current_jmi_request_peer[call.account] = this;
+
+            stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_accept_to_self(stream, sid);
+            stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_proceed_to_peer(stream, jid, sid);
+        }
+    }
+
+    public void reject() {
+        call.state = Call.State.DECLINED;
+
+        if (session != null) {
+            foreach (Xep.Jingle.Content content in session.contents) {
+                content.reject();
+            }
+        } else {
+            // Only a JMI so far
+            XmppStream stream = stream_interactor.get_stream(call.account);
+            if (stream == null) return;
+
+            stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_peer(stream, jid, sid);
+            stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_self(stream, sid);
+        }
+    }
+
+    public void end(string terminate_reason, string? reason_text = null) {
+        switch (terminate_reason) {
+            case Xep.Jingle.ReasonElement.SUCCESS:
+                if (session != null) {
+                    session.terminate(terminate_reason, reason_text, "success");
+                }
+                break;
+            case Xep.Jingle.ReasonElement.CANCEL:
+                if (session != null) {
+                    session.terminate(terminate_reason, reason_text, "cancel");
+                } else if (group_call != null) {
+                    // We don't have to do anything (?)
+                } else {
+                    // Only a JMI so far
+                    XmppStream? stream = stream_interactor.get_stream(call.account);
+                    if (stream == null) return;
+                    stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_retract_to_peer(stream, jid, sid);
+                }
+                break;
+        }
+    }
+
+    internal void mute_own_audio(bool mute) {
+        // Call isn't fully established yet. Audio will be muted once the stream is created.
+        if (session == null || audio_content_parameter == null || audio_content_parameter.stream == null) return;
+
+        Xep.JingleRtp.Stream stream = audio_content_parameter.stream;
+
+        // Inform our counterpart that we (un)muted our audio
+        stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_mute(session, mute, "audio");
+
+        // Start/Stop sending audio data
+        Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
+    }
+
+    internal void mute_own_video(bool mute) {
+
+        if (session == null) {
+            // Call hasn't been established yet
+            return;
+        }
+
+        Xep.JingleRtp.Module rtp_module = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY);
+
+        if (video_content_parameter != null &&
+                video_content_parameter.stream != null &&
+                session.senders_include_us(video_content.senders)) {
+            // A video content already exists
+
+            // Start/Stop sending video data
+            Xep.JingleRtp.Stream stream = video_content_parameter.stream;
+            if (stream != null) {
+                Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
+            }
+
+            // Inform our counterpart that we started/stopped our video
+            rtp_module.session_info_type.send_mute(session, mute, "video");
+        } else if (!mute) {
+            // Add a new video content
+            XmppStream stream = stream_interactor.get_stream(call.account);
+            rtp_module.add_outgoing_video_content.begin(stream, session, group_call != null ? group_call.muc_jid : null, (_, res) => {
+                if (video_content_parameter == null) {
+                    Xep.Jingle.Content content = rtp_module.add_outgoing_video_content.end(res);
+                    Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
+                    if (rtp_content_parameter != null) {
+                        connect_content_signals(content, rtp_content_parameter);
+                    }
+                }
+            });
+        }
+        // If video_content_parameter == null && !mute we're trying to mute a non-existant feed. It will be muted as soon as it is created.
+    }
+
+    public Xep.JingleRtp.Stream? get_video_stream(Call call) {
+        if (video_content_parameter != null) {
+            return video_content_parameter.stream;
+        }
+        return null;
+    }
+
+    public Xep.JingleRtp.Stream? get_audio_stream(Call call) {
+        if (audio_content_parameter != null) {
+            return audio_content_parameter.stream;
+        }
+        return null;
+    }
+
+    internal void set_session(Xep.Jingle.Session session) {
+        this.session = session;
+        this.sid = session.sid;
+
+        session.terminated.connect((stream, we_terminated, reason_name, reason_text) =>
+            session_terminated(we_terminated, reason_name, reason_text)
+        );
+        session.additional_content_add_incoming.connect((session,stream, content) =>
+            on_incoming_content_add(stream, session, content)
+        );
+
+        foreach (Xep.Jingle.Content content in session.contents) {
+            Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
+            if (rtp_content_parameter == null) continue;
+
+            connect_content_signals(content, rtp_content_parameter);
+        }
+    }
+
+    public PeerInfo get_info() {
+        var ret = new PeerInfo();
+
+        if (audio_content_parameter != null) {
+            ret.audio_rtcp_ready = audio_content_parameter.rtcp_ready;
+            ret.audio_rtp_ready = audio_content_parameter.rtp_ready;
+
+            if (audio_content_parameter.agreed_payload_type != null) {
+                ret.audio_codec = audio_content_parameter.agreed_payload_type.name;
+                ret.audio_clockrate = audio_content_parameter.agreed_payload_type.clockrate;
+            }
+        }
+
+        if (audio_content != null) {
+            Xmpp.Xep.Jingle.ComponentConnection? component0 = audio_content.get_transport_connection(1);
+            if (component0 != null) {
+                ret.audio_bytes_received = component0.bytes_received;
+                ret.audio_bytes_sent = component0.bytes_sent;
+            }
+        }
+
+        if (video_content_parameter != null) {
+            ret.video_content_exists = true;
+            ret.video_rtcp_ready = video_content_parameter.rtcp_ready;
+            ret.video_rtp_ready = video_content_parameter.rtp_ready;
+
+            if (video_content_parameter.agreed_payload_type != null) {
+                ret.video_codec = video_content_parameter.agreed_payload_type.name;
+            }
+        }
+
+        if (video_content != null) {
+            Xmpp.Xep.Jingle.ComponentConnection? component0 = video_content.get_transport_connection(1);
+            if (component0 != null) {
+                ret.video_bytes_received = component0.bytes_received;
+                ret.video_bytes_sent = component0.bytes_sent;
+            }
+        }
+        return ret;
+    }
+
+    private void connect_content_signals(Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) {
+        if (rtp_content_parameter.media == "audio") {
+            audio_content = content;
+            audio_content_parameter = rtp_content_parameter;
+        } else if (rtp_content_parameter.media == "video") {
+            video_content = content;
+            video_content_parameter = rtp_content_parameter;
+        }
+
+        debug(@"[%s] %s connecting content signals %s", call.account.bare_jid.to_string(), jid.to_string(), rtp_content_parameter.media);
+        rtp_content_parameter.stream_created.connect((stream) => on_stream_created(rtp_content_parameter.media, stream));
+        rtp_content_parameter.connection_ready.connect((status) => on_connection_ready(content, rtp_content_parameter.media));
+
+        content.senders_modify_incoming.connect((content, proposed_senders) => {
+            if (content.session.senders_include_us(content.senders) != content.session.senders_include_us(proposed_senders)) {
+                warning("counterpart set us to (not)sending %s. ignoring", content.content_name);
+                return;
+            }
+
+            if (!content.session.senders_include_counterpart(content.senders) && content.session.senders_include_counterpart(proposed_senders)) {
+                // Counterpart wants to start sending. Ok.
+                content.accept_content_modify(proposed_senders);
+                on_counterpart_mute_update(false, "video");
+            }
+        });
+    }
+
+    private void on_incoming_content_add(XmppStream stream, Xep.Jingle.Session session, Xep.Jingle.Content content) {
+        Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
+
+        if (rtp_content_parameter == null) {
+            content.reject();
+            return;
+        }
+
+        // Our peer shouldn't tell us to start sending, that's for us to initiate
+        if (session.senders_include_us(content.senders)) {
+            if (session.senders_include_counterpart(content.senders)) {
+                // If our peer wants to send, let them
+                content.modify(session.we_initiated ? Xep.Jingle.Senders.RESPONDER : Xep.Jingle.Senders.INITIATOR);
+            } else {
+                // If only we're supposed to send, reject
+                content.reject();
+            }
+        }
+
+        connect_content_signals(content, rtp_content_parameter);
+        content.accept();
+    }
+
+    private void on_stream_created(string media, Xep.JingleRtp.Stream stream) {
+        if (media == "video" && stream.receiving) {
+            counterpart_sends_video = true;
+            video_content_parameter.connection_ready.connect((status) => {
+                counterpart_sends_video_updated(false);
+            });
+        }
+
+        // Outgoing audio/video might have been muted in the meanwhile.
+        if (media == "video" && !we_should_send_video) {
+            mute_own_video(true);
+        } else if (media == "audio" && !we_should_send_audio) {
+            mute_own_audio(true);
+        }
+    }
+
+    private void on_counterpart_mute_update(bool mute, string? media) {
+        if (!call.equals(call)) return;
+
+        if (media == "video") {
+            counterpart_sends_video = !mute;
+            debug(@"[%s] %s video muted %s", call.account.bare_jid.to_string(), jid.to_string(), mute.to_string());
+            counterpart_sends_video_updated(mute);
+        }
+    }
+
+    private void on_connection_ready(Xep.Jingle.Content content, string media) {
+        debug("[%s] %s on_connection_ready", call.account.bare_jid.to_string(), jid.to_string());
+        connection_ready();
+
+        if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
+            call.state = Call.State.IN_PROGRESS;
+        }
+
+        if (media == "audio") {
+            audio_encryptions = content.encryptions;
+        } else if (media == "video") {
+            video_encryptions = content.encryptions;
+        }
+
+        if ((audio_encryptions != null && audio_encryptions.is_empty) || (video_encryptions != null && video_encryptions.is_empty)) {
+            call.encryption = Encryption.NONE;
+            encryption_updated(null, null, true);
+            return;
+        }
+
+        HashMap<string, Xep.Jingle.ContentEncryption> encryptions = audio_encryptions ?? video_encryptions;
+
+        Xep.Jingle.ContentEncryption? omemo_encryption = null, dtls_encryption = null, srtp_encryption = null;
+        foreach (string encr_name in encryptions.keys) {
+            if (video_encryptions != null && !video_encryptions.has_key(encr_name)) continue;
+
+            var encryption = encryptions[encr_name];
+            if (encryption.encryption_ns == "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification") {
+                omemo_encryption = encryption;
+            } else if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) {
+                dtls_encryption = encryption;
+            } else if (encryption.encryption_name == "SRTP") {
+                srtp_encryption = encryption;
+            }
+        }
+
+        if (omemo_encryption != null && dtls_encryption != null) {
+            call.encryption = Encryption.OMEMO;
+            omemo_encryption.peer_key = dtls_encryption.peer_key;
+            omemo_encryption.our_key = dtls_encryption.our_key;
+            audio_encryption = omemo_encryption;
+            encryption_keys_same = true;
+            video_encryption = video_encryptions != null ? video_encryptions["http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"] : null;
+        } else if (dtls_encryption != null) {
+            call.encryption = Encryption.DTLS_SRTP;
+            audio_encryption = dtls_encryption;
+            video_encryption = video_encryptions != null ? video_encryptions[Xep.JingleIceUdp.DTLS_NS_URI] : null;
+            encryption_keys_same = true;
+            if (video_encryption != null && dtls_encryption.peer_key.length == video_encryption.peer_key.length) {
+                for (int i = 0; i < dtls_encryption.peer_key.length; i++) {
+                    if (dtls_encryption.peer_key[i] != video_encryption.peer_key[i]) {
+                        encryption_keys_same = false;
+                        break;
+                    }
+                }
+            }
+        } else if (srtp_encryption != null) {
+            call.encryption = Encryption.SRTP;
+            audio_encryption = srtp_encryption;
+            video_encryption = video_encryptions != null ? video_encryptions["SRTP"] : null;
+            encryption_keys_same = false;
+        } else {
+            call.encryption = Encryption.NONE;
+            encryption_keys_same = true;
+        }
+
+        encryption_updated(audio_encryption, video_encryption, encryption_keys_same);
+    }
+}
+
+public class Dino.PeerInfo {
+    public bool audio_rtp_ready { get; set; }
+    public bool audio_rtcp_ready { get; set; }
+    public ulong? audio_bytes_sent { get; set; default=0; }
+    public ulong? audio_bytes_received { get; set; default=0; }
+    public string? audio_codec { get; set; }
+    public uint32 audio_clockrate { get; set; }
+
+    public bool video_content_exists { get; set; }
+    public bool video_rtp_ready { get; set; }
+    public bool video_rtcp_ready { get; set; }
+    public ulong? video_bytes_sent { get; set; default=0; }
+    public ulong? video_bytes_received { get; set; default=0; }
+    public string? video_codec { get; set; }
+}
\ No newline at end of file
diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala
new file mode 100644
index 00000000..385cf9dc
--- /dev/null
+++ b/libdino/src/service/call_state.vala
@@ -0,0 +1,302 @@
+using Dino.Entities;
+using Gee;
+using Xmpp;
+
+public class Dino.CallState : Object {
+
+    public signal void terminated(Jid who_terminated, string? reason_name, string? reason_text);
+    public signal void peer_joined(Jid jid, PeerState peer_state);
+    public signal void peer_left(Jid jid, PeerState peer_state, string? reason_name, string? reason_text);
+
+    public StreamInteractor stream_interactor;
+    public Call call;
+    public Xep.Muji.GroupCall? group_call { get; set; }
+    public Jid? invited_to_group_call = null;
+    public Jid? group_call_inviter = null;
+    public bool accepted { get; private set; default=false; }
+
+    public bool we_should_send_audio { get; set; default=false; }
+    public bool we_should_send_video { get; set; default=false; }
+    public HashMap<Jid, PeerState> peers = new HashMap<Jid, PeerState>(Jid.hash_func, Jid.equals_func);
+
+    public CallState(Call call, StreamInteractor stream_interactor) {
+        this.call = call;
+        this.stream_interactor = stream_interactor;
+
+        if (call.direction == Call.DIRECTION_OUTGOING) {
+            accepted = true;
+
+            Timeout.add_seconds(30, () => {
+                if (this == null) return false; // TODO enough?
+                if (call.state == Call.State.ESTABLISHING) {
+                    call.state = Call.State.MISSED;
+                    terminated(call.account.bare_jid, null, null);
+                }
+                return false;
+            });
+        }
+    }
+
+    internal PeerState set_first_peer(Jid peer) {
+        var peer_state = new PeerState(peer, call, stream_interactor);
+        peer_state.first_peer = true;
+        add_peer(peer_state);
+        return peer_state;
+    }
+
+    internal void add_peer(PeerState peer) {
+        call.add_peer(peer.jid.bare_jid);
+        connect_peer_signals(peer);
+        peer_joined(peer.jid, peer);
+    }
+
+    public void accept() {
+        accepted = true;
+        call.state = Call.State.ESTABLISHING;
+
+        if (invited_to_group_call != null) {
+            XmppStream stream = stream_interactor.get_stream(call.account);
+            if (stream == null) return;
+            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_accept_to_peer(stream, group_call_inviter, invited_to_group_call);
+            join_group_call.begin(invited_to_group_call);
+        } else {
+            foreach (PeerState peer in peers.values) {
+                peer.accept();
+            }
+        }
+    }
+
+    public void reject() {
+        call.state = Call.State.DECLINED;
+
+        if (invited_to_group_call != null) {
+            XmppStream stream = stream_interactor.get_stream(call.account);
+            if (stream == null) return;
+            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_self(stream, invited_to_group_call);
+            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_peer(stream, group_call_inviter, invited_to_group_call);
+        }
+        var peers_cpy = new ArrayList<PeerState>();
+        peers_cpy.add_all(peers.values);
+        foreach (PeerState peer in peers_cpy) {
+            peer.reject();
+        }
+        terminated(call.account.bare_jid, null, null);
+    }
+
+    public void end() {
+        var peers_cpy = new ArrayList<PeerState>();
+        peers_cpy.add_all(peers.values);
+
+        if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
+            foreach (PeerState peer in peers_cpy) {
+                peer.end(Xep.Jingle.ReasonElement.SUCCESS);
+            }
+            call.state = Call.State.ENDED;
+        } else if (call.state == Call.State.RINGING) {
+            foreach (PeerState peer in peers_cpy) {
+                peer.end(Xep.Jingle.ReasonElement.CANCEL);
+            }
+            call.state = Call.State.MISSED;
+        } else {
+            return;
+        }
+
+        call.end_time = new DateTime.now_utc();
+
+        terminated(call.account.bare_jid, null, null);
+    }
+
+    public void mute_own_audio(bool mute) {
+        we_should_send_audio = !mute;
+        foreach (PeerState peer in peers.values) {
+            peer.mute_own_audio(mute);
+        }
+    }
+
+    public void mute_own_video(bool mute) {
+        we_should_send_video = !mute;
+        foreach (PeerState peer in peers.values) {
+            peer.mute_own_video(mute);
+        }
+    }
+
+    public bool should_we_send_video() {
+        return we_should_send_video;
+    }
+
+    public async void invite_to_call(Jid invitee) {
+        if (this.group_call == null) yield convert_into_group_call();
+        if (this.group_call == null) return;
+
+        XmppStream stream = stream_interactor.get_stream(call.account);
+        if (stream == null) return;
+
+        debug("[%s] Inviting to muji call %s", call.account.bare_jid.to_string(), invitee.to_string());
+        yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, invitee, null, "owner");
+        stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, invitee, group_call.muc_jid, we_should_send_video);
+
+        // If the peer hasn't accepted within a minute, retract the invite
+        Timeout.add_seconds(60, () => {
+            if (this == null) return false;
+
+            bool contains_peer = false;
+            foreach (Jid peer in peers.keys) {
+                if (peer.equals_bare(invitee)) {
+                    contains_peer = true;
+                }
+            }
+
+            if (!contains_peer) {
+                debug("[%s] Retracting invite to %s from %s", call.account.bare_jid.to_string(), group_call.muc_jid.to_string(), invitee.to_string());
+                stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, invitee, group_call.muc_jid);
+                stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none");
+            }
+            return false;
+        });
+    }
+
+    internal void rename_peer(Jid from_jid, Jid to_jid) {
+        debug("[%s] Renaming %s to %s exists %b", call.account.bare_jid.to_string(), from_jid.to_string(), to_jid.to_string(), peers.has_key(from_jid));
+        PeerState? peer_state = peers[from_jid];
+        if (peer_state == null) return;
+
+        // Adjust the internal mapping of this `PeerState` object
+        peers.unset(from_jid);
+        peers[to_jid] = peer_state;
+        peer_state.jid = to_jid;
+    }
+
+    private void on_call_terminated(Jid who_terminated, bool we_terminated, string? reason_name, string? reason_text) {
+        if (call.state == Call.State.RINGING || call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
+            call.end_time = new DateTime.now_utc();
+        }
+        if (call.state == Call.State.IN_PROGRESS) {
+            call.state = Call.State.ENDED;
+        } else if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
+            if (reason_name == Xep.Jingle.ReasonElement.DECLINE) {
+                call.state = Call.State.DECLINED;
+            } else {
+                call.state = Call.State.FAILED;
+            }
+        }
+
+        terminated(who_terminated, reason_name, reason_text);
+    }
+
+    private void connect_peer_signals(PeerState peer_state) {
+        peers[peer_state.jid] = peer_state;
+
+        this.bind_property("we-should-send-audio", peer_state, "we-should-send-audio", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+        this.bind_property("we-should-send-video", peer_state, "we-should-send-video", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+        this.bind_property("group-call", peer_state, "group-call", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
+
+        peer_state.session_terminated.connect((we_terminated, reason_name, reason_text) => {
+            peers.unset(peer_state.jid);
+            debug("[%s] Peer left %s left %i", call.account.bare_jid.to_string(), peer_state.jid.to_string(), peers.size);
+
+            if (peers.is_empty) {
+                if (group_call != null) group_call.leave(stream_interactor.get_stream(call.account));
+                on_call_terminated(peer_state.jid, we_terminated, reason_name, reason_text);
+            } else {
+                peer_left(peer_state.jid, peer_state, reason_name, reason_text);
+            }
+        });
+    }
+
+    public async void convert_into_group_call() {
+        XmppStream stream = stream_interactor.get_stream(call.account);
+        if (stream == null) return;
+
+        Jid muc_jid = stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[call.account] ?? new Jid("chat.jabberfr.org");
+        muc_jid = new Jid("%08x@".printf(Random.next_int()) + muc_jid.to_string()); // TODO longer?
+
+        debug("[%s] Converting call to groupcall %s", call.account.bare_jid.to_string(), muc_jid.to_string());
+        yield join_group_call(muc_jid);
+
+        Xep.DataForms.DataForm? data_form = yield stream_interactor.get_module(MucManager.IDENTITY).get_config_form(call.account, muc_jid);
+        if (data_form == null) return;
+
+        foreach (Xep.DataForms.DataForm.Field field in data_form.fields) {
+            switch (field.var) {
+                case "muc#roomconfig_allowinvites":
+                    if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
+                        ((Xep.DataForms.DataForm.BooleanField) field).value = true;
+                    }
+                    break;
+                case "muc#roomconfig_persistentroom":
+                    if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
+                        ((Xep.DataForms.DataForm.BooleanField) field).value = false;
+                    }
+                    break;
+                case "muc#roomconfig_membersonly":
+                    if (field.type_ == Xep.DataForms.DataForm.Type.BOOLEAN) {
+                        ((Xep.DataForms.DataForm.BooleanField) field).value = true;
+                    }
+                    break;
+                case "muc#roomconfig_whois":
+                    if (field.type_ == Xep.DataForms.DataForm.Type.LIST_SINGLE) {
+                        ((Xep.DataForms.DataForm.ListSingleField) field).value = "anyone";
+                    }
+                    break;
+            }
+        }
+        yield stream_interactor.get_module(MucManager.IDENTITY).set_config_form(call.account, muc_jid, data_form);
+
+        foreach (Jid peer_jid in peers.keys) {
+            debug("[%s] Group call inviting %s", call.account.bare_jid.to_string(), peer_jid.to_string());
+            yield invite_to_call(peer_jid);
+        }
+    }
+
+    public async void join_group_call(Jid muc_jid) {
+        debug("[%s] Joining group call %s", call.account.bare_jid.to_string(), muc_jid.to_string());
+        XmppStream stream = stream_interactor.get_stream(call.account);
+        if (stream == null) return;
+
+        this.group_call = yield stream.get_module(Xep.Muji.Module.IDENTITY).join_call(stream, muc_jid, we_should_send_video);
+        if (this.group_call == null) {
+            warning("[%s] Couldn't join MUJI MUC", call.account.bare_jid.to_string());
+            return;
+        }
+
+        this.group_call.peer_joined.connect((jid) => {
+            debug("[%s] Group call peer joined: %s", call.account.bare_jid.to_string(), jid.to_string());
+
+            // Newly joined peers have to call us, not the other way round
+            // Maybe they called us already. Accept the call.
+            // (Except for the first peer, we already have a connection to that one.)
+            if (peers.has_key(jid)) {
+                if (!peers[jid].first_peer) {
+                    peers[jid].accept();
+                }
+                // else: Connection to first peer already active
+            } else {
+                var peer_state = new PeerState(jid, call, stream_interactor);
+                peer_state.waiting_for_inbound_muji_connection = true;
+                debug("[%s] Waiting for call from %s", call.account.bare_jid.to_string(), jid.to_string());
+                add_peer(peer_state);
+            }
+        });
+
+        this.group_call.peer_left.connect((jid) => {
+            debug("[%s] Group call peer left: %s", call.account.bare_jid.to_string(), jid.to_string());
+            if (!peers.has_key(jid)) return;
+            // end() will in the end cause a `peer_left` signal and removal from `peers`
+            peers[jid].end(Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
+        });
+
+        // Call all peers that are in the room already
+        foreach (Jid peer_jid in group_call.peers_to_connect_to) {
+            // Don't establish connection if we have one already (the person that invited us to the call)
+            if (peers.has_key(peer_jid)) continue;
+
+            debug("[%s] Calling %s because they were in the MUC already", call.account.bare_jid.to_string(), peer_jid.to_string());
+
+            PeerState peer_state = new PeerState(peer_jid, call, stream_interactor);
+            add_peer(peer_state);
+            peer_state.call_resource.begin(peer_jid);
+        }
+
+        debug("[%s] Finished joining MUJI muc %s", call.account.bare_jid.to_string(), muc_jid.to_string());
+    }
+}
\ No newline at end of file
diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala
index 365c15d9..51ed6e78 100644
--- a/libdino/src/service/calls.vala
+++ b/libdino/src/service/calls.vala
@@ -7,16 +7,11 @@ namespace Dino {
 
     public class Calls : StreamInteractionModule, Object {
 
-        public signal void call_incoming(Call call, Conversation conversation, bool video);
-        public signal void call_outgoing(Call call, Conversation conversation);
+        public signal void call_incoming(Call call, CallState state, Conversation conversation, bool video);
+        public signal void call_outgoing(Call call, CallState state, Conversation conversation);
 
         public signal void call_terminated(Call call, string? reason_name, string? reason_text);
-        public signal void counterpart_ringing(Call call);
-        public signal void counterpart_sends_video_updated(Call call, bool mute);
-        public signal void info_received(Call call, Xep.JingleRtp.CallSessionInfo session_info);
-        public signal void encryption_updated(Call call, Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption, bool same);
-
-        public signal void stream_created(Call call, string media);
+        public signal void conference_info_received(Call call, Xep.Coin.ConferenceInfo conference_info);
 
         public static ModuleIdentity<Calls> IDENTITY = new ModuleIdentity<Calls>("calls");
         public string id { get { return IDENTITY.id; } }
@@ -24,24 +19,9 @@ namespace Dino {
         private StreamInteractor stream_interactor;
         private Database db;
 
-        private HashMap<Account, HashMap<Call, string>> sid_by_call = new HashMap<Account, HashMap<Call, string>>(Account.hash_func, Account.equals_func);
-        private HashMap<Account, HashMap<string, Call>> call_by_sid = new HashMap<Account, HashMap<string, Call>>(Account.hash_func, Account.equals_func);
-        public HashMap<Call, Xep.Jingle.Session> sessions = new HashMap<Call, Xep.Jingle.Session>(Call.hash_func, Call.equals_func);
-
-        public HashMap<Account, Call> jmi_call = new HashMap<Account, Call>(Account.hash_func, Account.equals_func);
-        public HashMap<Account, string> jmi_sid = new HashMap<Account, string>(Account.hash_func, Account.equals_func);
-        public HashMap<Account, bool> jmi_video = new HashMap<Account, bool>(Account.hash_func, Account.equals_func);
-
-        private HashMap<Call, bool> counterpart_sends_video = new HashMap<Call, bool>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, bool> we_should_send_video = new HashMap<Call, bool>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, bool> we_should_send_audio = new HashMap<Call, bool>(Call.hash_func, Call.equals_func);
-
-        private HashMap<Call, Xep.JingleRtp.Parameters> audio_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, Xep.JingleRtp.Parameters> video_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, Xep.Jingle.Content> audio_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, Xep.Jingle.Content> video_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>> video_encryptions = new HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>>(Call.hash_func, Call.equals_func);
-        private HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>> audio_encryptions = new HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>>(Call.hash_func, Call.equals_func);
+        public HashMap<Account, CallState> current_jmi_request_call = new HashMap<Account, CallState>(Account.hash_func, Account.equals_func);
+        public HashMap<Account, PeerState> current_jmi_request_peer = new HashMap<Account, PeerState>(Account.hash_func, Account.equals_func);
+        public HashMap<Call, CallState> call_states = new HashMap<Call, CallState>(Call.hash_func, Call.equals_func);
 
         public static void start(StreamInteractor stream_interactor, Database db) {
             Calls m = new Calls(stream_interactor, db);
@@ -55,210 +35,35 @@ namespace Dino {
             stream_interactor.account_added.connect(on_account_added);
         }
 
-        public Xep.JingleRtp.Stream? get_video_stream(Call call) {
-            if (video_content_parameter.has_key(call)) {
-                return video_content_parameter[call].stream;
-            }
-            return null;
-        }
-
-        public Xep.JingleRtp.Stream? get_audio_stream(Call call) {
-            if (audio_content_parameter.has_key(call)) {
-                return audio_content_parameter[call].stream;
-            }
-            return null;
-        }
-
-        public async Call? initiate_call(Conversation conversation, bool video) {
+        public async CallState? initiate_call(Conversation conversation, bool video) {
             Call call = new Call();
             call.direction = Call.DIRECTION_OUTGOING;
             call.account = conversation.account;
-            call.counterpart = conversation.counterpart;
+            call.add_peer(conversation.counterpart);
             call.ourpart = conversation.account.full_jid;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
             call.state = Call.State.RINGING;
 
             stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
 
-            we_should_send_video[call] = video;
-            we_should_send_audio[call] = true;
-
-            Gee.List<Jid> call_resources = yield get_call_resources(conversation);
-
-            bool do_jmi = false;
-            Jid? jid_for_direct = null;
-            if (yield contains_jmi_resources(conversation.account, call_resources)) {
-                do_jmi = true;
-            } else if (!call_resources.is_empty) {
-                jid_for_direct = call_resources[0];
-            } else if (has_jmi_resources(conversation)) {
-                do_jmi = true;
-            }
-
-            if (do_jmi) {
-                XmppStream? stream = stream_interactor.get_stream(conversation.account);
-                jmi_call[conversation.account] = call;
-                jmi_video[conversation.account] = video;
-                jmi_sid[conversation.account] = Xmpp.random_uuid();
-
-                call_by_sid[call.account][jmi_sid[conversation.account]] = call;
+            var call_state = new CallState(call, stream_interactor);
+            call_state.we_should_send_video = video;
+            call_state.we_should_send_audio = true;
+            connect_call_state_signals(call_state);
+            PeerState peer_state = call_state.set_first_peer(conversation.counterpart);
 
-                var descriptions = new ArrayList<StanzaNode>();
-                descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
-                if (video) {
-                    descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "video"));
-                }
-
-                stream.get_module(Xmpp.Xep.JingleMessageInitiation.Module.IDENTITY).send_session_propose_to_peer(stream, conversation.counterpart, jmi_sid[call.account], descriptions);
-            } else if (jid_for_direct != null) {
-                yield call_resource(conversation.account, jid_for_direct, call, video);
-            }
+            yield peer_state.initiate_call(conversation.counterpart);
 
             conversation.last_active = call.time;
-            call_outgoing(call, conversation);
-
-            return call;
-        }
-
-        private async void call_resource(Account account, Jid full_jid, Call call, bool video, string? sid = null) {
-            XmppStream? stream = stream_interactor.get_stream(account);
-            if (stream == null) return;
-
-            try {
-                Xep.Jingle.Session session = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).start_call(stream, full_jid, video, sid);
-                sessions[call] = session;
-                sid_by_call[call.account][call] = session.sid;
-
-                connect_session_signals(call, session);
-            } catch (Error e) {
-                warning("Failed to start call: %s", e.message);
-            }
-        }
-
-        public void end_call(Conversation conversation, Call call) {
-            XmppStream? stream = stream_interactor.get_stream(call.account);
-            if (stream == null) return;
-
-            if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
-                sessions[call].terminate(Xep.Jingle.ReasonElement.SUCCESS, null, "success");
-                call.state = Call.State.ENDED;
-            } else if (call.state == Call.State.RINGING) {
-                if (sessions.has_key(call)) {
-                    sessions[call].terminate(Xep.Jingle.ReasonElement.CANCEL, null, "cancel");
-                } else {
-                    // Only a JMI so far
-                    stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_retract_to_peer(stream, call.counterpart, jmi_sid[call.account]);
-                }
-                call.state = Call.State.MISSED;
-            } else {
-                return;
-            }
-
-            call.end_time = new DateTime.now_utc();
-
-            remove_call_from_datastructures(call);
-        }
-
-        public void accept_call(Call call) {
-            call.state = Call.State.ESTABLISHING;
-
-            if (sessions.has_key(call)) {
-                foreach (Xep.Jingle.Content content in sessions[call].contents) {
-                    content.accept();
-                }
-            } else {
-                // Only a JMI so far
-                Account account = call.account;
-                string sid = sid_by_call[call.account][call];
-                XmppStream stream = stream_interactor.get_stream(account);
-                if (stream == null) return;
-
-                jmi_call[account] = call;
-                jmi_sid[account] = sid;
-                jmi_video[account] = we_should_send_video[call];
-
-                stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_accept_to_self(stream, sid);
-                stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_proceed_to_peer(stream, call.counterpart, sid);
-            }
-        }
-
-        public void reject_call(Call call) {
-            call.state = Call.State.DECLINED;
-
-            if (sessions.has_key(call)) {
-                foreach (Xep.Jingle.Content content in sessions[call].contents) {
-                    content.reject();
-                }
-                remove_call_from_datastructures(call);
-            } else {
-                // Only a JMI so far
-                XmppStream stream = stream_interactor.get_stream(call.account);
-                if (stream == null) return;
-
-                string sid = sid_by_call[call.account][call];
-                stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_peer(stream, call.counterpart, sid);
-                stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_self(stream, sid);
-                remove_call_from_datastructures(call);
-            }
-        }
-
-        public void mute_own_audio(Call call, bool mute) {
-            we_should_send_audio[call] = !mute;
-
-            Xep.JingleRtp.Stream stream = audio_content_parameter[call].stream;
-            // The user might mute audio before a feed was created. The feed will be muted as soon as it has been created.
-            if (stream == null) return;
-
-            // Inform our counterpart that we (un)muted our audio
-            stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_mute(sessions[call], mute, "audio");
-
-            // Start/Stop sending audio data
-            Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
-        }
-
-        public void mute_own_video(Call call, bool mute) {
-            we_should_send_video[call] = !mute;
-
-            if (!sessions.has_key(call)) {
-                // Call hasn't been established yet
-                return;
-            }
-
-            Xep.JingleRtp.Module rtp_module = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY);
 
-            if (video_content_parameter.has_key(call) &&
-                    video_content_parameter[call].stream != null &&
-                    sessions[call].senders_include_us(video_content[call].senders)) {
-                // A video feed has already been established
+            call_outgoing(call, call_state, conversation);
 
-                // Start/Stop sending video data
-                Xep.JingleRtp.Stream stream = video_content_parameter[call].stream;
-                if (stream != null) {
-                    // TODO maybe the user muted video before the feed was created...
-                    Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
-                }
-
-                // Inform our counterpart that we started/stopped our video
-                rtp_module.session_info_type.send_mute(sessions[call], mute, "video");
-            } else if (!mute) {
-                // Need to start a new video feed
-                XmppStream stream = stream_interactor.get_stream(call.account);
-                rtp_module.add_outgoing_video_content.begin(stream, sessions[call], (_, res) => {
-                    if (video_content_parameter[call] == null) {
-                        Xep.Jingle.Content content = rtp_module.add_outgoing_video_content.end(res);
-                        Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
-                        if (rtp_content_parameter != null) {
-                            connect_content_signals(call, content, rtp_content_parameter);
-                        }
-                    }
-                });
-            }
-            // If video_feed == null && !mute we're trying to mute a non-existant feed. It will be muted as soon as it is created.
+            return call_state;
         }
 
         public async bool can_do_audio_calls_async(Conversation conversation) {
             if (!can_do_audio_calls()) return false;
-            return (yield get_call_resources(conversation)).size > 0 || has_jmi_resources(conversation);
+            return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
         }
 
         private bool can_do_audio_calls() {
@@ -270,7 +75,7 @@ namespace Dino {
 
         public async bool can_do_video_calls_async(Conversation conversation) {
             if (!can_do_video_calls()) return false;
-            return (yield get_call_resources(conversation)).size > 0 || has_jmi_resources(conversation);
+            return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
         }
 
         private bool can_do_video_calls() {
@@ -280,13 +85,13 @@ namespace Dino {
             return plugin.supports("video");
         }
 
-        private async Gee.List<Jid> get_call_resources(Conversation conversation) {
+        public async Gee.List<Jid> get_call_resources(Account account, Jid counterpart) {
             ArrayList<Jid> ret = new ArrayList<Jid>();
 
-            XmppStream? stream = stream_interactor.get_stream(conversation.account);
+            XmppStream? stream = stream_interactor.get_stream(account);
             if (stream == null) return ret;
 
-            Gee.List<Jid>? full_jids = stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart);
+            Gee.List<Jid>? full_jids = stream.get_flag(Presence.Flag.IDENTITY).get_resources(counterpart);
             if (full_jids == null) return ret;
 
             foreach (Jid full_jid in full_jids) {
@@ -297,7 +102,7 @@ namespace Dino {
             return ret;
         }
 
-        private async bool contains_jmi_resources(Account account, Gee.List<Jid> full_jids) {
+        public async bool contains_jmi_resources(Account account, Gee.List<Jid> full_jids) {
             XmppStream? stream = stream_interactor.get_stream(account);
             if (stream == null) return false;
 
@@ -308,26 +113,22 @@ namespace Dino {
             return false;
         }
 
-        private bool has_jmi_resources(Conversation conversation) {
+        public bool has_jmi_resources(Jid counterpart) {
             int64 jmi_resources = db.entity.select()
-                    .with(db.entity.jid_id, "=", db.get_jid_id(conversation.counterpart))
+                    .with(db.entity.jid_id, "=", db.get_jid_id(counterpart))
                     .join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity)
                     .with(db.entity_feature.feature, "=", Xep.JingleMessageInitiation.NS_URI)
                     .count();
             return jmi_resources > 0;
         }
 
-        public bool should_we_send_video(Call call) {
-            return we_should_send_video[call];
-        }
-
-        public Jid? is_call_in_progress() {
-            foreach (Call call in sessions.keys) {
+        public bool is_call_in_progress() {
+            foreach (Call call in call_states.keys) {
                 if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
-                    return call.counterpart;
+                    return true;
                 }
             }
-            return null;
+            return false;
         }
 
         private void on_incoming_call(Account account, Xep.Jingle.Session session) {
@@ -336,265 +137,168 @@ namespace Dino {
                 return;
             }
 
+            Jid? muji_muc = null;
             bool counterpart_wants_video = false;
             foreach (Xep.Jingle.Content content in session.contents) {
                 Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
                 if (rtp_content_parameter == null) continue;
+                muji_muc = rtp_content_parameter.muji_muc;
                 if (rtp_content_parameter.media == "video" && session.senders_include_us(content.senders)) {
                     counterpart_wants_video = true;
                 }
             }
 
-            // Session might have already been accepted via Jingle Message Initiation
-            bool already_accepted = jmi_sid.has_key(account) &&
-                    jmi_sid[account] == session.sid && jmi_call[account].account.equals(account) &&
-                    jmi_call[account].counterpart.equals_bare(session.peer_full_jid) &&
-                    jmi_video[account] == counterpart_wants_video;
-
-            Call? call = null;
-            if (already_accepted) {
-                call = jmi_call[account];
-            } else {
-                call = create_received_call(account, session.peer_full_jid, account.full_jid, counterpart_wants_video);
+            // Check if this comes from a MUJI MUC => accept
+            if (muji_muc != null) {
+                debug("[%s] Incoming call from %s from MUJI muc %s", account.bare_jid.to_string(), session.peer_full_jid.to_string(), muji_muc.to_string());
+
+                foreach (CallState call_state in call_states.values) {
+                    if (call_state.group_call != null && call_state.group_call.muc_jid.equals(muji_muc)) {
+                        if (call_state.peers.keys.contains(session.peer_full_jid)) {
+                            PeerState peer_state = call_state.peers[session.peer_full_jid];
+                            debug("[%s] Incoming call, we know the peer. Expected %b", account.bare_jid.to_string(), peer_state.waiting_for_inbound_muji_connection);
+                            if (!peer_state.waiting_for_inbound_muji_connection) return;
+
+                            peer_state.set_session(session);
+                            debug(@"[%s] Accepting incoming MUJI call from %s", account.bare_jid.to_string(), session.peer_full_jid.to_string());
+                            peer_state.accept();
+                        } else {
+                            debug(@"[%s] Incoming call, but didn't see peer in MUC yet", account.bare_jid.to_string());
+                            PeerState peer_state = new PeerState(session.peer_full_jid, call_state.call, stream_interactor);
+                            peer_state.set_session(session);
+                            call_state.add_peer(peer_state);
+                        }
+                        return;
+                    }
+                }
+                return;
             }
-            sessions[call] = session;
 
-            call_by_sid[account][session.sid] = call;
-            sid_by_call[account][call] = session.sid;
+            debug(@"[%s] Incoming call from %s", account.bare_jid.to_string(), session.peer_full_jid.to_string());
 
-            connect_session_signals(call, session);
+            // Check if we already accepted this call via Jingle Message Initiation => accept
+            if (current_jmi_request_call.has_key(account) &&
+                    current_jmi_request_peer[account].sid == session.sid &&
+                    current_jmi_request_peer[account].we_should_send_video == counterpart_wants_video &&
+                    current_jmi_request_peer[account].accepted_jmi) {
+                current_jmi_request_peer[account].set_session(session);
+                current_jmi_request_call[account].accept();
 
-            if (already_accepted) {
-                accept_call(call);
-            } else {
-                stream_interactor.module_manager.get_module(account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_ringing(session);
+                current_jmi_request_peer.unset(account);
+                current_jmi_request_call.unset(account);
+                return;
             }
+
+            // This is a direct call without prior JMI. Ask user.
+            PeerState peer_state = create_received_call(account, session.peer_full_jid, account.full_jid, counterpart_wants_video);
+            peer_state.set_session(session);
+            stream_interactor.module_manager.get_module(account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_ringing(session);
         }
 
-        private Call create_received_call(Account account, Jid from, Jid to, bool video_requested) {
+        private PeerState create_received_call(Account account, Jid from, Jid to, bool video_requested) {
             Call call = new Call();
+            Jid counterpart = null;
             if (from.equals_bare(account.bare_jid)) {
                 // Call requested by another of our devices
                 call.direction = Call.DIRECTION_OUTGOING;
                 call.ourpart = from;
-                call.counterpart = to;
+                counterpart = to;
             } else {
                 call.direction = Call.DIRECTION_INCOMING;
                 call.ourpart = account.full_jid;
-                call.counterpart = from;
+                counterpart = from;
             }
+            call.add_peer(counterpart);
             call.account = account;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
             call.state = Call.State.RINGING;
 
-            Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT);
+            Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(counterpart.bare_jid, account, Conversation.Type.CHAT);
 
             stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
 
             conversation.last_active = call.time;
 
-            we_should_send_video[call] = video_requested;
-            we_should_send_audio[call] = true;
+            var call_state = new CallState(call, stream_interactor);
+            connect_call_state_signals(call_state);
+            PeerState peer_state = call_state.set_first_peer(counterpart);
+            call_state.we_should_send_video = video_requested;
+            call_state.we_should_send_audio = true;
 
             if (call.direction == Call.DIRECTION_INCOMING) {
-                call_incoming(call, conversation, video_requested);
+                call_incoming(call, call_state, conversation, video_requested);
             } else {
-                call_outgoing(call, conversation);
+                call_outgoing(call, call_state, conversation);
             }
 
-            return call;
+            return peer_state;
         }
 
-        private void on_incoming_content_add(XmppStream stream, Call call, Xep.Jingle.Session session, Xep.Jingle.Content content) {
-            Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
-
-            if (rtp_content_parameter == null) {
-                content.reject();
-                return;
-            }
-
-            // Our peer shouldn't tell us to start sending, that's for us to initiate
-            if (session.senders_include_us(content.senders)) {
-                if (session.senders_include_counterpart(content.senders)) {
-                    // If our peer wants to send, let them
-                    content.modify(session.we_initiated ? Xep.Jingle.Senders.RESPONDER : Xep.Jingle.Senders.INITIATOR);
-                } else {
-                    // If only we're supposed to send, reject
-                    content.reject();
+        private CallState? get_call_state_for_groupcall(Account account, Jid muc_jid) {
+            foreach (CallState call_state in call_states.values) {
+                if (call_state.group_call != null && call_state.call.account.equals(account) && call_state.group_call.muc_jid.equals(muc_jid)) {
+                    return call_state;
                 }
             }
-
-            connect_content_signals(call, content, rtp_content_parameter);
-            content.accept();
+            return null;
         }
 
-        private void on_call_terminated(Call call, bool we_terminated, string? reason_name, string? reason_text) {
-            if (call.state == Call.State.RINGING || call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
-                call.end_time = new DateTime.now_utc();
-            }
-            if (call.state == Call.State.IN_PROGRESS) {
-                call.state = Call.State.ENDED;
-            } else if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
-                if (reason_name == Xep.Jingle.ReasonElement.DECLINE) {
-                    call.state = Call.State.DECLINED;
-                } else {
-                    call.state = Call.State.FAILED;
+        private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, Gee.List<StanzaNode> descriptions) {
+            debug("[%s] on_muji_call_received", account.bare_jid.to_string());
+            foreach (Call call in call_states.keys) {
+                if (call.account.equals(account) && call.counterparts.contains(inviter_jid) && call_states[call].accepted) {
+                    // A call is converted into a group call.
+                    yield call_states[call].join_group_call(muc_jid);
+                    return;
                 }
             }
 
-            call_terminated(call, reason_name, reason_text);
-            remove_call_from_datastructures(call);
-        }
+            bool audio_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "audio");
+            bool video_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "video");
 
-        private void on_stream_created(Call call, string media, Xep.JingleRtp.Stream stream) {
-            if (media == "video" && stream.receiving) {
-                counterpart_sends_video[call] = true;
-                video_content_parameter[call].connection_ready.connect((status) => {
-                    counterpart_sends_video_updated(call, false);
-                });
-            }
-            stream_created(call, media);
+            Call call = new Call();
+            call.direction = Call.DIRECTION_INCOMING;
+            call.ourpart = account.full_jid;
+            call.add_peer(inviter_jid); // not rly
+            call.account = account;
+            call.time = call.local_time = call.end_time = new DateTime.now_utc();
+            call.state = Call.State.RINGING;
 
-            // Outgoing audio/video might have been muted in the meanwhile.
-            if (media == "video" && !we_should_send_video[call]) {
-                mute_own_video(call, true);
-            } else if (media == "audio" && !we_should_send_audio[call]) {
-                mute_own_audio(call, true);
-            }
-        }
+            Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(inviter_jid.bare_jid, account);
+            stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
+            conversation.last_active = call.time;
 
-        private void on_counterpart_mute_update(Call call, bool mute, string? media) {
-            if (!call.equals(call)) return;
+            CallState call_state = new CallState(call, stream_interactor);
+            connect_call_state_signals(call_state);
+            call_state.we_should_send_audio = true;
+            call_state.we_should_send_video = video_requested;
+            call_state.invited_to_group_call = muc_jid;
+            call_state.group_call_inviter = inviter_jid;
 
-            if (media == "video") {
-                counterpart_sends_video[call] = !mute;
-                counterpart_sends_video_updated(call, mute);
-            }
+            debug("[%s] on_muji_call_received accepting", account.bare_jid.to_string());
+            call_incoming(call_state.call, call_state, conversation, video_requested);
         }
 
-        private void connect_session_signals(Call call, Xep.Jingle.Session session) {
-            session.terminated.connect((stream, we_terminated, reason_name, reason_text) =>
-                on_call_terminated(call, we_terminated, reason_name, reason_text)
-            );
-            session.additional_content_add_incoming.connect((session,stream, content) =>
-                on_incoming_content_add(stream, call, session, content)
-            );
-
-            foreach (Xep.Jingle.Content content in session.contents) {
-                Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
-                if (rtp_content_parameter == null) continue;
-
-                connect_content_signals(call, content, rtp_content_parameter);
+        private void remove_call_from_datastructures(Call call) {
+            if (current_jmi_request_call.has_key(call.account) && current_jmi_request_call[call.account].call.equals(call)) {
+                current_jmi_request_call.unset(call.account);
+                current_jmi_request_peer.unset(call.account);
             }
+            call_states.unset(call);
         }
 
-        private void connect_content_signals(Call call, Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) {
-            if (rtp_content_parameter.media == "audio") {
-                audio_content[call] = content;
-                audio_content_parameter[call] = rtp_content_parameter;
-            } else if (rtp_content_parameter.media == "video") {
-                video_content[call] = content;
-                video_content_parameter[call] = rtp_content_parameter;
-            }
-
-            rtp_content_parameter.stream_created.connect((stream) => on_stream_created(call, rtp_content_parameter.media, stream));
-            rtp_content_parameter.connection_ready.connect((status) => on_connection_ready(call, content, rtp_content_parameter.media));
-
-            content.senders_modify_incoming.connect((content, proposed_senders) => {
-                if (content.session.senders_include_us(content.senders) != content.session.senders_include_us(proposed_senders)) {
-                    warning("counterpart set us to (not)sending %s. ignoring", content.content_name);
-                    return;
-                }
+        private void connect_call_state_signals(CallState call_state) {
+            call_states[call_state.call] = call_state;
 
-                if (!content.session.senders_include_counterpart(content.senders) && content.session.senders_include_counterpart(proposed_senders)) {
-                    // Counterpart wants to start sending. Ok.
-                    content.accept_content_modify(proposed_senders);
-                    on_counterpart_mute_update(call, false, "video");
-                }
+            ulong terminated_handler_id = -1;
+            terminated_handler_id = call_state.terminated.connect((who_terminated, reason_name, reason_text) => {
+                remove_call_from_datastructures(call_state.call);
+                call_terminated(call_state.call, reason_name, reason_text);
+                call_state.disconnect(terminated_handler_id);
             });
         }
 
-        private void on_connection_ready(Call call, Xep.Jingle.Content content, string media) {
-            if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
-                call.state = Call.State.IN_PROGRESS;
-            }
-
-            if (media == "audio") {
-                audio_encryptions[call] = content.encryptions;
-            } else if (media == "video") {
-                video_encryptions[call] = content.encryptions;
-            }
-
-            if ((audio_encryptions.has_key(call) && audio_encryptions[call].is_empty) || (video_encryptions.has_key(call) && video_encryptions[call].is_empty)) {
-                call.encryption = Encryption.NONE;
-                encryption_updated(call, null, null, true);
-                return;
-            }
-
-            HashMap<string, Xep.Jingle.ContentEncryption> encryptions = audio_encryptions[call] ?? video_encryptions[call];
-
-            Xep.Jingle.ContentEncryption? omemo_encryption = null, dtls_encryption = null, srtp_encryption = null;
-            foreach (string encr_name in encryptions.keys) {
-                if (video_encryptions.has_key(call) && !video_encryptions[call].has_key(encr_name)) continue;
-
-                var encryption = encryptions[encr_name];
-                if (encryption.encryption_ns == "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification") {
-                    omemo_encryption = encryption;
-                } else if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) {
-                    dtls_encryption = encryption;
-                } else if (encryption.encryption_name == "SRTP") {
-                    srtp_encryption = encryption;
-                }
-            }
-
-            if (omemo_encryption != null && dtls_encryption != null) {
-                call.encryption = Encryption.OMEMO;
-                Xep.Jingle.ContentEncryption? video_encryption = video_encryptions.has_key(call) ? video_encryptions[call]["http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"] : null;
-                omemo_encryption.peer_key = dtls_encryption.peer_key;
-                omemo_encryption.our_key = dtls_encryption.our_key;
-                encryption_updated(call, omemo_encryption, video_encryption, true);
-            } else if (dtls_encryption != null) {
-                call.encryption = Encryption.DTLS_SRTP;
-                Xep.Jingle.ContentEncryption? video_encryption = video_encryptions.has_key(call) ? video_encryptions[call][Xep.JingleIceUdp.DTLS_NS_URI] : null;
-                bool same = true;
-                if (video_encryption != null && dtls_encryption.peer_key.length == video_encryption.peer_key.length) {
-                    for (int i = 0; i < dtls_encryption.peer_key.length; i++) {
-                        if (dtls_encryption.peer_key[i] != video_encryption.peer_key[i]) { same = false; break; }
-                    }
-                }
-                encryption_updated(call, dtls_encryption, video_encryption, same);
-            } else if (srtp_encryption != null) {
-                call.encryption = Encryption.SRTP;
-                encryption_updated(call, srtp_encryption, video_encryptions[call]["SRTP"], false);
-            } else {
-                call.encryption = Encryption.NONE;
-                encryption_updated(call, null, null, true);
-            }
-        }
-
-        private void remove_call_from_datastructures(Call call) {
-            string? sid = sid_by_call[call.account][call];
-            sid_by_call[call.account].unset(call);
-            if (sid != null) call_by_sid[call.account].unset(sid);
-
-            sessions.unset(call);
-
-            counterpart_sends_video.unset(call);
-            we_should_send_video.unset(call);
-            we_should_send_audio.unset(call);
-
-            audio_content_parameter.unset(call);
-            video_content_parameter.unset(call);
-            audio_content.unset(call);
-            video_content.unset(call);
-            audio_encryptions.unset(call);
-            video_encryptions.unset(call);
-        }
-
         private void on_account_added(Account account) {
-            call_by_sid[account] = new HashMap<string, Call>();
-            sid_by_call[account] = new HashMap<Call, string>();
-
             Xep.Jingle.Module jingle_module = stream_interactor.module_manager.get_module(account, Xep.Jingle.Module.IDENTITY);
             jingle_module.session_initiate_received.connect((stream, session) => {
                 foreach (Xep.Jingle.Content content in session.contents) {
@@ -606,27 +310,6 @@ namespace Dino {
                 }
             });
 
-            var session_info_type = stream_interactor.module_manager.get_module(account, Xep.JingleRtp.Module.IDENTITY).session_info_type;
-            session_info_type.mute_update_received.connect((session,mute, name) => {
-                if (!call_by_sid[account].has_key(session.sid)) return;
-                Call call = call_by_sid[account][session.sid];
-
-                foreach (Xep.Jingle.Content content in session.contents) {
-                    if (name == null || content.content_name == name) {
-                        Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
-                        if (rtp_content_parameter != null) {
-                            on_counterpart_mute_update(call, mute, rtp_content_parameter.media);
-                        }
-                    }
-                }
-            });
-            session_info_type.info_received.connect((session, session_info) => {
-                if (!call_by_sid[account].has_key(session.sid)) return;
-                Call call = call_by_sid[account][session.sid];
-
-                info_received(call, session_info);
-            });
-
             Xep.JingleMessageInitiation.Module mi_module = stream_interactor.module_manager.get_module(account, Xep.JingleMessageInitiation.Module.IDENTITY);
             mi_module.session_proposed.connect((from, to, sid, descriptions) => {
                 if (!can_do_audio_calls()) {
@@ -637,53 +320,94 @@ namespace Dino {
                 bool audio_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "audio");
                 bool video_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "video");
                 if (!audio_requested && !video_requested) return;
-                Call call = create_received_call(account, from, to, video_requested);
-                call_by_sid[account][sid] = call;
-                sid_by_call[account][call] = sid;
+
+                PeerState peer_state = create_received_call(account, from, to, video_requested);
+                peer_state.sid = sid;
+                peer_state.we_should_send_audio = true;
+                peer_state.we_should_send_video = video_requested;
+
+                current_jmi_request_peer[account] = peer_state;
+                current_jmi_request_call[account] = call_states[peer_state.call];
             });
             mi_module.session_accepted.connect((from, sid) => {
-                if (!call_by_sid[account].has_key(sid)) return;
+                if (!current_jmi_request_peer.has_key(account) || current_jmi_request_peer[account].sid != sid) return;
 
                 if (from.equals_bare(account.bare_jid)) { // Carboned message from our account
                     // Ignore carbon from ourselves
                     if (from.equals(account.full_jid)) return;
 
-                    Call call = call_by_sid[account][sid];
+                    Call call = current_jmi_request_peer[account].call;
                     call.state = Call.State.OTHER_DEVICE_ACCEPTED;
                     remove_call_from_datastructures(call);
-                } else if (from.equals_bare(call_by_sid[account][sid].counterpart)) { // Message from our peer
+                } else if (from.equals_bare(current_jmi_request_peer[account].jid)) { // Message from our peer
                     // We proposed the call
-                    if (jmi_sid.has_key(account) && jmi_sid[account] == sid) {
-                        call_resource.begin(account, from, jmi_call[account], jmi_video[account], jmi_sid[account]);
-                        jmi_call.unset(account);
-                        jmi_sid.unset(account);
-                        jmi_video.unset(account);
-                    }
+                    // We know the full jid of our peer now
+                    current_jmi_request_call[account].rename_peer(current_jmi_request_peer[account].jid, from);
+                    current_jmi_request_peer[account].call_resource(from);
                 }
             });
             mi_module.session_rejected.connect((from, to, sid) => {
-                if (!call_by_sid[account].has_key(sid)) return;
-                Call call = call_by_sid[account][sid];
+                if (!current_jmi_request_peer.has_key(account) || current_jmi_request_peer[account].sid != sid) return;
+                Call call = current_jmi_request_peer[account].call;
 
-                bool outgoing_reject = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(call.counterpart);
+                bool outgoing_reject = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(call.counterparts[0]);
                 bool incoming_reject = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(account.bare_jid);
-                if (!(outgoing_reject || incoming_reject)) return;
+                if (!outgoing_reject && !incoming_reject) return;
 
                 call.state = Call.State.DECLINED;
+                call_states[call].terminated(from, Xep.Jingle.ReasonElement.DECLINE, "JMI reject");
                 remove_call_from_datastructures(call);
-                call_terminated(call, null, null);
             });
             mi_module.session_retracted.connect((from, to, sid) => {
-                if (!call_by_sid[account].has_key(sid)) return;
-                Call call = call_by_sid[account][sid];
+                if (!current_jmi_request_peer.has_key(account) || current_jmi_request_peer[account].sid != sid) return;
+                Call call = current_jmi_request_peer[account].call;
 
-                bool outgoing_retract = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(call.counterpart);
-                bool incoming_retract = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(account.bare_jid);
+                bool outgoing_retract = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(account.bare_jid);
+                bool incoming_retract = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(call.counterparts[0]);
                 if (!(outgoing_retract || incoming_retract)) return;
 
                 call.state = Call.State.MISSED;
+                call_states[call].terminated(from, Xep.Jingle.ReasonElement.CANCEL, "JMI retract");
                 remove_call_from_datastructures(call);
-                call_terminated(call, null, null);
+            });
+
+            Xep.MujiMeta.Module muji_meta_module = stream_interactor.module_manager.get_module(account, Xep.MujiMeta.Module.IDENTITY);
+            muji_meta_module.call_proposed.connect((inviter_jid, to, muc_jid, descriptions) => {
+                if (inviter_jid.equals_bare(account.bare_jid)) return;
+                on_muji_call_received.begin(account, inviter_jid, muc_jid, descriptions);
+            });
+            muji_meta_module.call_accepted.connect((from_jid, muc_jid) => {
+                if (!from_jid.equals_bare(account.bare_jid)) return;
+
+                // We accepted the call from another device
+                CallState? call_state = get_call_state_for_groupcall(account, muc_jid);
+                if (call_state == null) return;
+
+                call_state.call.state = Call.State.OTHER_DEVICE_ACCEPTED;
+                remove_call_from_datastructures(call_state.call);
+            });
+            muji_meta_module.call_retracted.connect((from_jid, muc_jid) => {
+                if (from_jid.equals_bare(account.bare_jid)) return;
+
+                // The call was retracted by the counterpart
+                CallState? call_state = get_call_state_for_groupcall(account, muc_jid);
+                if (call_state == null) return;
+
+                call_state.call.state = Call.State.MISSED;
+                remove_call_from_datastructures(call_state.call);
+            });
+            muji_meta_module.call_rejected.connect((from_jid, to_jid, muc_jid) => {
+                if (from_jid.equals_bare(account.bare_jid)) return;
+                debug(@"[%s] rejected our MUJI invite to %s", account.bare_jid.to_string(), from_jid.to_string(), muc_jid.to_string());
+            });
+
+            stream_interactor.module_manager.get_module(account, Xep.Coin.Module.IDENTITY).coin_info_received.connect((jid, info) => {
+                foreach (Call call in call_states.keys) {
+                    if (call.counterparts[0].equals_bare(jid)) {
+                        conference_info_received(call, info);
+                        return;
+                    }
+                }
             });
         }
     }
diff --git a/libdino/src/service/connection_manager.vala b/libdino/src/service/connection_manager.vala
index 0eb6a6f5..d114b9ae 100644
--- a/libdino/src/service/connection_manager.vala
+++ b/libdino/src/service/connection_manager.vala
@@ -228,6 +228,8 @@ public class ConnectionManager : Object {
         stream.attached_modules.connect((stream) => {
             stream_attached_modules(account, stream);
             change_connection_state(account, ConnectionState.CONNECTED);
+
+//            stream.get_module(Xep.Muji.Module.IDENTITY).join_call(stream, new Jid("test@muc.poez.io"), true);
         });
         stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
             set_connection_error(account, new ConnectionError(ConnectionError.Source.SASL, null));
diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala
index ddb6571a..e0102d24 100644
--- a/libdino/src/service/content_item_store.vala
+++ b/libdino/src/service/content_item_store.vala
@@ -186,7 +186,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
         }
     }
 
-    private void insert_call(Call call, Conversation conversation) {
+    private void insert_call(Call call, CallState call_state, Conversation conversation) {
         CallItem item = new CallItem(call, conversation, -1);
         item.id = db.add_content_item(conversation, call.time, call.local_time, 3, call.id, false);
         if (collection_conversations.has_key(conversation)) {
@@ -321,7 +321,7 @@ public class CallItem : ContentItem {
     public Conversation conversation;
 
     public CallItem(Call call, Conversation conversation, int id) {
-        base(id, TYPE, call.from, call.time, call.encryption, Message.Marked.NONE);
+        base(id, TYPE, call.direction == Call.DIRECTION_OUTGOING ? conversation.account.bare_jid : conversation.counterpart, call.time, call.encryption, Message.Marked.NONE);
 
         this.call = call;
         this.conversation = conversation;
diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala
index dab32749..0300112a 100644
--- a/libdino/src/service/database.vala
+++ b/libdino/src/service/database.vala
@@ -7,7 +7,7 @@ using Dino.Entities;
 namespace Dino {
 
 public class Database : Qlite.Database {
-    private const int VERSION = 21;
+    private const int VERSION = 22;
 
     public class AccountTable : Table {
         public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
@@ -174,6 +174,19 @@ public class Database : Qlite.Database {
         }
     }
 
+    public class CallCounterpartTable : Table {
+        public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
+        public Column<int> call_id = new Column.Integer("call_id") { not_null = true };
+        public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
+        public Column<string> resource = new Column.Text("resource");
+
+        internal CallCounterpartTable(Database db) {
+            base(db, "call_counterpart");
+            init({call_id, jid_id, resource});
+            index("call_counterpart_call_jid_idx", {call_id});
+        }
+    }
+
     public class ConversationTable : Table {
         public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
         public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
@@ -295,6 +308,7 @@ public class Database : Qlite.Database {
     public RealJidTable real_jid { get; private set; }
     public FileTransferTable file_transfer { get; private set; }
     public CallTable call { get; private set; }
+    public CallCounterpartTable call_counterpart { get; private set; }
     public ConversationTable conversation { get; private set; }
     public AvatarTable avatar { get; private set; }
     public EntityIdentityTable entity_identity { get; private set; }
@@ -319,6 +333,7 @@ public class Database : Qlite.Database {
         real_jid = new RealJidTable(this);
         file_transfer = new FileTransferTable(this);
         call = new CallTable(this);
+        call_counterpart = new CallCounterpartTable(this);
         conversation = new ConversationTable(this);
         avatar = new AvatarTable(this);
         entity_identity = new EntityIdentityTable(this);
@@ -327,7 +342,7 @@ public class Database : Qlite.Database {
         mam_catchup = new MamCatchupTable(this);
         settings = new SettingsTable(this);
         conversation_settings = new ConversationSettingsTable(this);
-        init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, call, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings, conversation_settings });
+        init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings, conversation_settings });
 
         try {
             exec("PRAGMA journal_mode = WAL");
@@ -446,6 +461,19 @@ public class Database : Qlite.Database {
                 error("Failed to upgrade to database version 18: %s", e.message);
             }
         }
+        if (oldVersion < 22) {
+            try {
+                exec("INSERT INTO call_counterpart (call_id, jid_id, resource) SELECT id, counterpart_id, counterpart_resource FROM call");
+            } catch (Error e) {
+                error("Failed to upgrade to database version 22: %s", e.message);
+            }
+//                exec("ALTER TABLE call RENAME TO call2");
+//                call.create_table_at_version(VERSION);
+//                exec("INSERT INTO call (id, account_id, our_resource, direction, time, local_time, end_time, encryption, state)
+//                            SELECT id, account_id, our_resource, direction, time, local_time, end_time, encryption, state
+//                            FROM call2");
+//                exec("DROP TABLE call2");
+        }
     }
 
     public ArrayList<Account> get_accounts() {
diff --git a/libdino/src/service/module_manager.vala b/libdino/src/service/module_manager.vala
index a6165392..39ed8a7c 100644
--- a/libdino/src/service/module_manager.vala
+++ b/libdino/src/service/module_manager.vala
@@ -80,6 +80,10 @@ public class ModuleManager {
             module_map[account].add(new Xep.LastMessageCorrection.Module());
             module_map[account].add(new Xep.DirectMucInvitations.Module());
             module_map[account].add(new Xep.JingleMessageInitiation.Module());
+            module_map[account].add(new Xep.JingleRawUdp.Module());
+            module_map[account].add(new Xep.Muji.Module());
+            module_map[account].add(new Xep.MujiMeta.Module());
+            module_map[account].add(new Xep.Coin.Module());
             initialize_account_modules(account, module_map[account]);
         }
     }
diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala
index 5a224a18..8a317a08 100644
--- a/libdino/src/service/muc_manager.vala
+++ b/libdino/src/service/muc_manager.vala
@@ -27,6 +27,7 @@ public class MucManager : StreamInteractionModule, Object {
     private ReceivedMessageListener received_message_listener;
     private HashMap<Account, BookmarksProvider> bookmarks_provider = new HashMap<Account, BookmarksProvider>(Account.hash_func, Account.equals_func);
     private HashMap<Account, Gee.List<Jid>> invites = new HashMap<Account, Gee.List<Jid>>(Account.hash_func, Account.equals_func);
+    public HashMap<Account, Jid> default_muc_server = new HashMap<Account, Jid>(Account.hash_func, Account.equals_func);
 
     public static void start(StreamInteractor stream_interactor) {
         MucManager m = new MucManager(stream_interactor);
@@ -76,7 +77,7 @@ public class MucManager : StreamInteractionModule, Object {
         }
         mucs_todo[account].add(jid.with_resource(nick_));
 
-        Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since);
+        Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, null);
 
         mucs_joining[account].remove(jid);
 
@@ -117,10 +118,10 @@ public class MucManager : StreamInteractionModule, Object {
         return yield stream.get_module(Xep.Muc.Module.IDENTITY).get_config_form(stream, jid);
     }
 
-    public void set_config_form(Account account, Jid jid, DataForms.DataForm data_form) {
+    public async void set_config_form(Account account, Jid jid, DataForms.DataForm data_form) {
         XmppStream? stream = stream_interactor.get_stream(account);
         if (stream == null) return;
-        stream.get_module(Xep.Muc.Module.IDENTITY).set_config_form(stream, jid, data_form);
+        yield stream.get_module(Xep.Muc.Module.IDENTITY).set_config_form(stream, jid, data_form);
     }
 
     public void change_subject(Account account, Jid jid, string subject) {
@@ -170,7 +171,7 @@ public class MucManager : StreamInteractionModule, Object {
 
     public void change_affiliation(Account account, Jid jid, string nick, string role) {
         XmppStream? stream = stream_interactor.get_stream(account);
-        if (stream != null) stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, jid.bare_jid, nick, role);
+        if (stream != null) stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, jid.bare_jid, null, nick, role);
     }
 
     public void change_role(Account account, Jid jid, string nick, string role) {
@@ -401,6 +402,36 @@ public class MucManager : StreamInteractionModule, Object {
         });
     }
 
+    private async void search_default_muc_server(Account account) {
+        XmppStream? stream = stream_interactor.get_stream(account);
+        if (stream == null) return;
+
+        ServiceDiscovery.ItemsResult? items_result = yield stream.get_module(ServiceDiscovery.Module.IDENTITY).request_items(stream, stream.remote_name);
+        if (items_result == null) return;
+
+        for (int i = 0; i < 2; i++) {
+            foreach (Xep.ServiceDiscovery.Item item in items_result.items) {
+
+                // First try the promising items and only afterwards all the others
+                bool promising_upload_item = item.jid.to_string().has_prefix("conference") ||
+                        item.jid.to_string().has_prefix("muc") ||
+                        item.jid.to_string().has_prefix("chat");
+                if ((i == 0 && !promising_upload_item) || (i == 1) && promising_upload_item) continue;
+
+                Gee.Set<Xep.ServiceDiscovery.Identity> identities = yield stream_interactor.get_module(EntityInfo.IDENTITY).get_identities(account, item.jid);
+                if (identities == null) return;
+
+                foreach (Xep.ServiceDiscovery.Identity identity in identities) {
+                    if (identity.category == Xep.ServiceDiscovery.Identity.CATEGORY_CONFERENCE) {
+                        default_muc_server[account] = item.jid;
+                        print(@"$(account.bare_jid) Default MUC: $(item.jid)\n");
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
     private async void on_stream_negotiated(Account account, XmppStream stream) {
         if (bookmarks_provider[account] == null) return;
 
@@ -411,6 +442,10 @@ public class MucManager : StreamInteractionModule, Object {
         } else {
             sync_autojoin_active(account, conferences);
         }
+
+        if (!default_muc_server.has_key(account)) {
+            search_default_muc_server.begin(account);
+        }
     }
 
     private void on_invite_received(Account account, Jid room_jid, Jid from_jid, string? password, string? reason) {
diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala
index f87ebe0d..6f1d0fd4 100644
--- a/libdino/src/service/notification_events.vala
+++ b/libdino/src/service/notification_events.vala
@@ -106,7 +106,7 @@ public class NotificationEvents : StreamInteractionModule, Object {
         notifier.notify_subscription_request.begin(conversation);
     }
 
-    private void on_call_incoming(Call call, Conversation conversation, bool video) {
+    private void on_call_incoming(Call call, CallState call_state, Conversation conversation, bool video) {
         string conversation_display_name = get_conversation_display_name(stream_interactor, conversation, null);
 
         notifier.notify_call.begin(call, conversation, video, conversation_display_name);
-- 
cgit v1.2.3-70-g09d2


From e205743f0cb71e32eecf183d067a4f34a7a89d48 Mon Sep 17 00:00:00 2001
From: fiaxh <git@lightrise.org>
Date: Thu, 11 Nov 2021 21:54:55 +0100
Subject: Display target bitrates in connection details UI

---
 libdino/src/service/call_peer_state.vala           | 12 ++++++++
 .../call_connection_details_window.vala            | 36 +++++++++++++++++-----
 plugins/rtp/src/stream.vala                        | 17 +++++-----
 .../src/module/xep/0167_jingle_rtp/stream.vala     |  8 ++++-
 4 files changed, 55 insertions(+), 18 deletions(-)

(limited to 'libdino/src')

diff --git a/libdino/src/service/call_peer_state.vala b/libdino/src/service/call_peer_state.vala
index ddd0d8dd..c7bcd201 100644
--- a/libdino/src/service/call_peer_state.vala
+++ b/libdino/src/service/call_peer_state.vala
@@ -260,6 +260,10 @@ public class Dino.PeerState : Object {
                 ret.audio_codec = audio_content_parameter.agreed_payload_type.name;
                 ret.audio_clockrate = audio_content_parameter.agreed_payload_type.clockrate;
             }
+            if (audio_content_parameter.stream != null && audio_content_parameter.stream.remb_enabled) {
+                ret.audio_target_receive_bitrate = audio_content_parameter.stream.target_receive_bitrate;
+                ret.audio_target_send_bitrate = audio_content_parameter.stream.target_send_bitrate;
+            }
         }
 
         if (audio_content != null) {
@@ -278,6 +282,10 @@ public class Dino.PeerState : Object {
             if (video_content_parameter.agreed_payload_type != null) {
                 ret.video_codec = video_content_parameter.agreed_payload_type.name;
             }
+            if (video_content_parameter.stream != null && video_content_parameter.stream.remb_enabled) {
+                ret.video_target_receive_bitrate = video_content_parameter.stream.target_receive_bitrate;
+                ret.video_target_send_bitrate = video_content_parameter.stream.target_send_bitrate;
+            }
         }
 
         if (video_content != null) {
@@ -443,6 +451,8 @@ public class Dino.PeerInfo {
     public ulong? audio_bytes_received { get; set; default=0; }
     public string? audio_codec { get; set; }
     public uint32 audio_clockrate { get; set; }
+    public uint audio_target_receive_bitrate { get; set; default=0; }
+    public uint audio_target_send_bitrate { get; set; default=0; }
 
     public bool video_content_exists { get; set; }
     public bool video_rtp_ready { get; set; }
@@ -450,4 +460,6 @@ public class Dino.PeerInfo {
     public ulong? video_bytes_sent { get; set; default=0; }
     public ulong? video_bytes_received { get; set; default=0; }
     public string? video_codec { get; set; }
+    public uint video_target_receive_bitrate { get; set; default=0; }
+    public uint video_target_send_bitrate { get; set; default=0; }
 }
\ No newline at end of file
diff --git a/main/src/ui/call_window/call_connection_details_window.vala b/main/src/ui/call_window/call_connection_details_window.vala
index b292b971..95e00296 100644
--- a/main/src/ui/call_window/call_connection_details_window.vala
+++ b/main/src/ui/call_window/call_connection_details_window.vala
@@ -8,15 +8,19 @@ namespace Dino.Ui {
 
         public Label audio_rtp_ready = new Label("?") { xalign=0, visible=true };
         public Label audio_rtcp_ready = new Label("?") { xalign=0, visible=true };
-        public Label audio_sent_bps = new Label("?") { xalign=0, visible=true };
-        public Label audio_recv_bps = new Label("?") { xalign=0, visible=true };
+        public Label audio_sent_bps = new Label("?") { use_markup=true, xalign=0, visible=true };
+        public Label audio_recv_bps = new Label("?") { use_markup=true, xalign=0, visible=true };
         public Label audio_codec = new Label("?") { xalign=0, visible=true };
+        public Label audio_target_receive_bitrate = new Label("n/a") { xalign=0, visible=true };
+        public Label audio_target_send_bitrate = new Label("n/a") { xalign=0, visible=true };
 
         public Label video_rtp_ready = new Label("") { xalign=0, visible=true };
         public Label video_rtcp_ready = new Label("") { xalign=0, visible=true };
-        public Label video_sent_bps = new Label("") { xalign=0, visible=true };
-        public Label video_recv_bps = new Label("") { xalign=0, visible=true };
+        public Label video_sent_bps = new Label("") { use_markup=true, xalign=0, visible=true };
+        public Label video_recv_bps = new Label("") { use_markup=true, xalign=0, visible=true };
         public Label video_codec = new Label("") { xalign=0, visible=true };
+        public Label video_target_receive_bitrate = new Label("n/a") { xalign=0, visible=true };
+        public Label video_target_send_bitrate = new Label("n/a") { xalign=0, visible=true };
 
         private int row_at = 0;
         private bool video_added = false;
@@ -34,6 +38,10 @@ namespace Dino.Ui {
                 grid.attach(audio_recv_bps, 1, row_at++, 1, 1);
                 put_row("Codec");
                 grid.attach(audio_codec, 1, row_at++, 1, 1);
+                put_row("Target receive bitrate");
+                grid.attach(audio_target_receive_bitrate, 1, row_at++, 1, 1);
+                put_row("Target send bitrate");
+                grid.attach(audio_target_send_bitrate, 1, row_at++, 1, 1);
 
                 this.child = grid;
         }
@@ -46,18 +54,26 @@ namespace Dino.Ui {
                 audio_rtp_ready.label = peer_info.audio_rtp_ready.to_string();
                 audio_rtcp_ready.label = peer_info.audio_rtcp_ready.to_string();
                 audio_codec.label = peer_info.audio_codec + " " + peer_info.audio_clockrate.to_string();
+                audio_target_receive_bitrate.label = peer_info.audio_target_receive_bitrate.to_string();
+                audio_target_send_bitrate.label = peer_info.audio_target_send_bitrate.to_string();
 
                 video_rtp_ready.label = peer_info.video_rtp_ready.to_string();
                 video_rtcp_ready.label = peer_info.video_rtcp_ready.to_string();
                 video_codec.label = peer_info.video_codec;
+                video_target_receive_bitrate.label = peer_info.video_target_receive_bitrate.to_string();
+                video_target_send_bitrate.label = peer_info.video_target_send_bitrate.to_string();
 
                 if (peer_info.video_content_exists) add_video_widgets();
 
                 if (prev_peer_info != null) {
-                        audio_sent_bps.label = (peer_info.audio_bytes_sent - prev_peer_info.audio_bytes_sent).to_string();
-                        audio_recv_bps.label = (peer_info.audio_bytes_received - prev_peer_info.audio_bytes_received).to_string();
-                        video_sent_bps.label = (peer_info.video_bytes_sent - prev_peer_info.video_bytes_sent).to_string();
-                        video_recv_bps.label = (peer_info.video_bytes_received - prev_peer_info.video_bytes_received).to_string();
+                        ulong audio_sent_kbps = (peer_info.audio_bytes_sent - prev_peer_info.audio_bytes_sent) * 8 / 1000;
+                        audio_sent_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(audio_sent_kbps);
+                        ulong audio_recv_kbps = (peer_info.audio_bytes_received - prev_peer_info.audio_bytes_received) * 8 / 1000;
+                        audio_recv_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(audio_recv_kbps);
+                        ulong video_sent_kbps = (peer_info.video_bytes_sent - prev_peer_info.video_bytes_sent) * 8 / 1000;
+                        video_sent_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(video_sent_kbps);
+                        ulong video_recv_kbps = (peer_info.video_bytes_received - prev_peer_info.video_bytes_received) * 8 / 1000;
+                        video_recv_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(video_recv_kbps);
                 }
                 prev_peer_info = peer_info;
         }
@@ -76,6 +92,10 @@ namespace Dino.Ui {
                 grid.attach(video_recv_bps, 1, row_at++, 1, 1);
                 put_row("Codec");
                 grid.attach(video_codec, 1, row_at++, 1, 1);
+                put_row("Target receive bitrate");
+                grid.attach(video_target_receive_bitrate, 1, row_at++, 1, 1);
+                put_row("Target send bitrate");
+                grid.attach(video_target_send_bitrate, 1, row_at++, 1, 1);
 
                 video_added = true;
         }
diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala
index 17c955a5..bfe5ae28 100644
--- a/plugins/rtp/src/stream.vala
+++ b/plugins/rtp/src/stream.vala
@@ -153,7 +153,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
         plugin.unpause();
 
         GLib.Signal.emit_by_name(rtpbin, "get-session", rtpid, out session);
-        if (session != null && payload_type.rtcp_fbs.any_match((it) => it.type_ == "goog-remb")) {
+        if (session != null && remb_enabled) {
             Object internal_session;
             session.@get("internal-session", out internal_session);
             if (internal_session != null) {
@@ -166,7 +166,6 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
         }
     }
 
-    private uint remb = 256;
     private int last_packets_lost = -1;
     private uint64 last_packets_received;
     private uint64 last_octets_received;
@@ -212,12 +211,12 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
                 if (new_received == 0) continue;
                 double loss_rate = (double)new_lost / (double)(new_lost + new_received);
                 if (new_lost <= 0 || loss_rate < 0.02) {
-                    remb = (uint)(1.08 * (double)remb);
+                    target_receive_bitrate = (uint)(1.08 * (double)target_receive_bitrate);
                 } else if (loss_rate > 0.1) {
-                    remb = (uint)((1.0 - 0.5 * loss_rate) * (double)remb);
+                    target_receive_bitrate = (uint)((1.0 - 0.5 * loss_rate) * (double)target_receive_bitrate);
                 }
-                remb = uint.max(remb, (uint)((new_octets * 8) / 1000));
-                remb = uint.max(16, remb); // Never go below 16
+                target_receive_bitrate = uint.max(target_receive_bitrate, (uint)((new_octets * 8) / 1000));
+                target_receive_bitrate = uint.max(16, target_receive_bitrate); // Never go below 16
                 uint8[] data = new uint8[] {
                     143, 206, 0, 5,
                     0, 0, 0, 0,
@@ -231,7 +230,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
                 data[6] = (uint8)((our_ssrc >> 8) & 0xff);
                 data[7] = (uint8)(our_ssrc & 0xff);
                 uint8 br_exp = 0;
-                uint32 br_mant = remb * 1000;
+                uint32 br_mant = target_receive_bitrate * 1000;
                 uint8 bits = (uint8)Math.log2(br_mant);
                 if (bits > 16) {
                     br_exp = (uint8)bits - 16;
@@ -258,8 +257,8 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
             if (data[0] != 'R' || data[1] != 'E' || data[2] != 'M' || data[3] != 'B') return;
             uint8 br_exp = data[5] >> 2;
             uint32 br_mant = (((uint32)data[5] & 0x3) << 16) + ((uint32)data[6] << 8) + (uint32)data[7];
-            uint bitrate = (br_mant << br_exp) / 1000;
-            self.input_device.update_bitrate(self.payload_type, bitrate);
+            self.target_send_bitrate = (br_mant << br_exp) / 1000;
+            self.input_device.update_bitrate(self.payload_type, self.target_send_bitrate);
         }
     }
 
diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala
index 65be8a0a..031f0ad0 100644
--- a/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala
+++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/stream.vala
@@ -1,5 +1,4 @@
 public abstract class Xmpp.Xep.JingleRtp.Stream : Object {
-
     public Jingle.Content content { get; protected set; }
 
     public string name { get {
@@ -54,6 +53,13 @@ public abstract class Xmpp.Xep.JingleRtp.Stream : Object {
         return false;
     }}
 
+    // Receiver Estimated Maximum Bitrate
+    public bool remb_enabled { get {
+        return payload_type != null ? payload_type.rtcp_fbs.any_match((it) => it.type_ == "goog-remb") : false;
+    }}
+    public uint target_receive_bitrate { get; set; default=256; }
+    public uint target_send_bitrate { get; set; default=256; }
+
     protected Stream(Jingle.Content content) {
         this.content = content;
     }
-- 
cgit v1.2.3-70-g09d2


From 2b3d150949fe1b3c4107e497be7dac8e2ba734aa Mon Sep 17 00:00:00 2001
From: fiaxh <git@lightrise.org>
Date: Mon, 15 Nov 2021 13:29:13 +0100
Subject: Improve call details dialog + small multi-party call fixes

---
 libdino/src/entity/call.vala                       |   2 +-
 libdino/src/service/call_peer_state.vala           |  86 +++++-----
 libdino/src/service/calls.vala                     |  11 +-
 .../call_connection_details_window.vala            | 175 +++++++++++----------
 .../ui/conversation_content_view/call_widget.vala  |   2 +-
 plugins/rtp/src/stream.vala                        |   2 +-
 .../xep/0167_jingle_rtp/content_parameters.vala    |   1 +
 .../module/xep/0353_jingle_message_initiation.vala |   4 +-
 8 files changed, 139 insertions(+), 144 deletions(-)

(limited to 'libdino/src')

diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala
index a5ab672e..5221a807 100644
--- a/libdino/src/entity/call.vala
+++ b/libdino/src/entity/call.vala
@@ -11,7 +11,7 @@ namespace Dino.Entities {
             RINGING,
             ESTABLISHING,
             IN_PROGRESS,
-            OTHER_DEVICE_ACCEPTED,
+            OTHER_DEVICE,
             ENDED,
             DECLINED,
             MISSED,
diff --git a/libdino/src/service/call_peer_state.vala b/libdino/src/service/call_peer_state.vala
index c7bcd201..8c4b0930 100644
--- a/libdino/src/service/call_peer_state.vala
+++ b/libdino/src/service/call_peer_state.vala
@@ -251,53 +251,43 @@ public class Dino.PeerState : Object {
 
     public PeerInfo get_info() {
         var ret = new PeerInfo();
-
-        if (audio_content_parameter != null) {
-            ret.audio_rtcp_ready = audio_content_parameter.rtcp_ready;
-            ret.audio_rtp_ready = audio_content_parameter.rtp_ready;
-
-            if (audio_content_parameter.agreed_payload_type != null) {
-                ret.audio_codec = audio_content_parameter.agreed_payload_type.name;
-                ret.audio_clockrate = audio_content_parameter.agreed_payload_type.clockrate;
-            }
-            if (audio_content_parameter.stream != null && audio_content_parameter.stream.remb_enabled) {
-                ret.audio_target_receive_bitrate = audio_content_parameter.stream.target_receive_bitrate;
-                ret.audio_target_send_bitrate = audio_content_parameter.stream.target_send_bitrate;
-            }
+        if (audio_content != null || audio_content_parameter != null) {
+            ret.audio = get_content_info(audio_content, audio_content_parameter);
         }
-
-        if (audio_content != null) {
-            Xmpp.Xep.Jingle.ComponentConnection? component0 = audio_content.get_transport_connection(1);
-            if (component0 != null) {
-                ret.audio_bytes_received = component0.bytes_received;
-                ret.audio_bytes_sent = component0.bytes_sent;
-            }
+        if (video_content != null || video_content_parameter != null) {
+            ret.video = get_content_info(video_content, video_content_parameter);
         }
+        return ret;
+    }
 
-        if (video_content_parameter != null) {
-            ret.video_content_exists = true;
-            ret.video_rtcp_ready = video_content_parameter.rtcp_ready;
-            ret.video_rtp_ready = video_content_parameter.rtp_ready;
+    private PeerContentInfo get_content_info(Xep.Jingle.Content? content, Xep.JingleRtp.Parameters? parameter) {
+        PeerContentInfo ret = new PeerContentInfo();
+        if (parameter != null) {
+            ret.rtcp_ready = parameter.rtcp_ready;
+            ret.rtp_ready = parameter.rtp_ready;
 
-            if (video_content_parameter.agreed_payload_type != null) {
-                ret.video_codec = video_content_parameter.agreed_payload_type.name;
+            if (parameter.agreed_payload_type != null) {
+                ret.codec = parameter.agreed_payload_type.name;
+                ret.clockrate = parameter.agreed_payload_type.clockrate;
             }
-            if (video_content_parameter.stream != null && video_content_parameter.stream.remb_enabled) {
-                ret.video_target_receive_bitrate = video_content_parameter.stream.target_receive_bitrate;
-                ret.video_target_send_bitrate = video_content_parameter.stream.target_send_bitrate;
+            if (parameter.stream != null && parameter.stream.remb_enabled) {
+                ret.target_receive_bytes = parameter.stream.target_receive_bitrate;
+                ret.target_send_bytes = parameter.stream.target_send_bitrate;
             }
         }
 
-        if (video_content != null) {
-            Xmpp.Xep.Jingle.ComponentConnection? component0 = video_content.get_transport_connection(1);
+        if (content != null) {
+            Xmpp.Xep.Jingle.ComponentConnection? component0 = content.get_transport_connection(1);
             if (component0 != null) {
-                ret.video_bytes_received = component0.bytes_received;
-                ret.video_bytes_sent = component0.bytes_sent;
+                ret.bytes_received = component0.bytes_received;
+                ret.bytes_sent = component0.bytes_sent;
             }
         }
         return ret;
     }
 
+
+
     private void connect_content_signals(Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) {
         if (rtp_content_parameter.media == "audio") {
             audio_content = content;
@@ -444,22 +434,18 @@ public class Dino.PeerState : Object {
     }
 }
 
+public class Dino.PeerContentInfo {
+    public bool rtp_ready { get; set; }
+    public bool rtcp_ready { get; set; }
+    public ulong? bytes_sent { get; set; default=0; }
+    public ulong? bytes_received { get; set; default=0; }
+    public string? codec { get; set; }
+    public uint32 clockrate { get; set; }
+    public uint target_receive_bytes { get; set; default=-1; }
+    public uint target_send_bytes { get; set; default=-1; }
+}
+
 public class Dino.PeerInfo {
-    public bool audio_rtp_ready { get; set; }
-    public bool audio_rtcp_ready { get; set; }
-    public ulong? audio_bytes_sent { get; set; default=0; }
-    public ulong? audio_bytes_received { get; set; default=0; }
-    public string? audio_codec { get; set; }
-    public uint32 audio_clockrate { get; set; }
-    public uint audio_target_receive_bitrate { get; set; default=0; }
-    public uint audio_target_send_bitrate { get; set; default=0; }
-
-    public bool video_content_exists { get; set; }
-    public bool video_rtp_ready { get; set; }
-    public bool video_rtcp_ready { get; set; }
-    public ulong? video_bytes_sent { get; set; default=0; }
-    public ulong? video_bytes_received { get; set; default=0; }
-    public string? video_codec { get; set; }
-    public uint video_target_receive_bitrate { get; set; default=0; }
-    public uint video_target_send_bitrate { get; set; default=0; }
+    public PeerContentInfo? audio = null;
+    public PeerContentInfo? video = null;
 }
\ No newline at end of file
diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala
index 51ed6e78..3d1ed7e8 100644
--- a/libdino/src/service/calls.vala
+++ b/libdino/src/service/calls.vala
@@ -202,16 +202,17 @@ namespace Dino {
                 // Call requested by another of our devices
                 call.direction = Call.DIRECTION_OUTGOING;
                 call.ourpart = from;
+                call.state = Call.State.OTHER_DEVICE;
                 counterpart = to;
             } else {
                 call.direction = Call.DIRECTION_INCOMING;
                 call.ourpart = account.full_jid;
+                call.state = Call.State.RINGING;
                 counterpart = from;
             }
             call.add_peer(counterpart);
             call.account = account;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
-            call.state = Call.State.RINGING;
 
             Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(counterpart.bare_jid, account, Conversation.Type.CHAT);
 
@@ -329,7 +330,7 @@ namespace Dino {
                 current_jmi_request_peer[account] = peer_state;
                 current_jmi_request_call[account] = call_states[peer_state.call];
             });
-            mi_module.session_accepted.connect((from, sid) => {
+            mi_module.session_accepted.connect((from, to, sid) => {
                 if (!current_jmi_request_peer.has_key(account) || current_jmi_request_peer[account].sid != sid) return;
 
                 if (from.equals_bare(account.bare_jid)) { // Carboned message from our account
@@ -337,9 +338,9 @@ namespace Dino {
                     if (from.equals(account.full_jid)) return;
 
                     Call call = current_jmi_request_peer[account].call;
-                    call.state = Call.State.OTHER_DEVICE_ACCEPTED;
+                    call.state = Call.State.OTHER_DEVICE;
                     remove_call_from_datastructures(call);
-                } else if (from.equals_bare(current_jmi_request_peer[account].jid)) { // Message from our peer
+                } else if (from.equals_bare(current_jmi_request_peer[account].jid) && to.equals(account.full_jid)) { // Message from our peer
                     // We proposed the call
                     // We know the full jid of our peer now
                     current_jmi_request_call[account].rename_peer(current_jmi_request_peer[account].jid, from);
@@ -383,7 +384,7 @@ namespace Dino {
                 CallState? call_state = get_call_state_for_groupcall(account, muc_jid);
                 if (call_state == null) return;
 
-                call_state.call.state = Call.State.OTHER_DEVICE_ACCEPTED;
+                call_state.call.state = Call.State.OTHER_DEVICE;
                 remove_call_from_datastructures(call_state.call);
             });
             muji_meta_module.call_retracted.connect((from_jid, muc_jid) => {
diff --git a/main/src/ui/call_window/call_connection_details_window.vala b/main/src/ui/call_window/call_connection_details_window.vala
index 95e00296..1d5265c9 100644
--- a/main/src/ui/call_window/call_connection_details_window.vala
+++ b/main/src/ui/call_window/call_connection_details_window.vala
@@ -4,100 +4,107 @@ namespace Dino.Ui {
 
     public class CallConnectionDetailsWindow : Gtk.Window {
 
-        public Grid grid = new Grid() { column_spacing=5, margin=10, halign=Align.CENTER, valign=Align.CENTER, visible=true };
-
-        public Label audio_rtp_ready = new Label("?") { xalign=0, visible=true };
-        public Label audio_rtcp_ready = new Label("?") { xalign=0, visible=true };
-        public Label audio_sent_bps = new Label("?") { use_markup=true, xalign=0, visible=true };
-        public Label audio_recv_bps = new Label("?") { use_markup=true, xalign=0, visible=true };
-        public Label audio_codec = new Label("?") { xalign=0, visible=true };
-        public Label audio_target_receive_bitrate = new Label("n/a") { xalign=0, visible=true };
-        public Label audio_target_send_bitrate = new Label("n/a") { xalign=0, visible=true };
-
-        public Label video_rtp_ready = new Label("") { xalign=0, visible=true };
-        public Label video_rtcp_ready = new Label("") { xalign=0, visible=true };
-        public Label video_sent_bps = new Label("") { use_markup=true, xalign=0, visible=true };
-        public Label video_recv_bps = new Label("") { use_markup=true, xalign=0, visible=true };
-        public Label video_codec = new Label("") { xalign=0, visible=true };
-        public Label video_target_receive_bitrate = new Label("n/a") { xalign=0, visible=true };
-        public Label video_target_send_bitrate = new Label("n/a") { xalign=0, visible=true };
+        public Box box = new Box(Orientation.VERTICAL, 15) { margin=10, halign=Align.CENTER, valign=Align.CENTER, visible=true };
 
-        private int row_at = 0;
         private bool video_added = false;
-        private PeerInfo? prev_peer_info = null;
+        private CallContentDetails audio_details = new CallContentDetails("Audio") { visible=true };
+        private CallContentDetails video_details = new CallContentDetails("Video");
 
         public CallConnectionDetailsWindow() {
-                grid.attach(new Label("<b>Audio</b>") { use_markup=true, xalign=0, visible=true }, 0, row_at++, 1, 1);
-                put_row("RTP");
-                grid.attach(audio_rtp_ready, 1, row_at++, 1, 1);
-                put_row("RTCP");
-                grid.attach(audio_rtcp_ready, 1, row_at++, 1, 1);
-                put_row("Sent bp/s");
-                grid.attach(audio_sent_bps, 1, row_at++, 1, 1);
-                put_row("Received bp/s");
-                grid.attach(audio_recv_bps, 1, row_at++, 1, 1);
-                put_row("Codec");
-                grid.attach(audio_codec, 1, row_at++, 1, 1);
-                put_row("Target receive bitrate");
-                grid.attach(audio_target_receive_bitrate, 1, row_at++, 1, 1);
-                put_row("Target send bitrate");
-                grid.attach(audio_target_send_bitrate, 1, row_at++, 1, 1);
-
-                this.child = grid;
-        }
-
-        private void put_row(string label) {
-            grid.attach(new Label(label) { xalign=0, visible=true }, 0, row_at, 1, 1);
+            box.add(audio_details);
+            box.add(video_details);
+            add(box);
         }
 
         public void update_content(PeerInfo peer_info) {
-                audio_rtp_ready.label = peer_info.audio_rtp_ready.to_string();
-                audio_rtcp_ready.label = peer_info.audio_rtcp_ready.to_string();
-                audio_codec.label = peer_info.audio_codec + " " + peer_info.audio_clockrate.to_string();
-                audio_target_receive_bitrate.label = peer_info.audio_target_receive_bitrate.to_string();
-                audio_target_send_bitrate.label = peer_info.audio_target_send_bitrate.to_string();
-
-                video_rtp_ready.label = peer_info.video_rtp_ready.to_string();
-                video_rtcp_ready.label = peer_info.video_rtcp_ready.to_string();
-                video_codec.label = peer_info.video_codec;
-                video_target_receive_bitrate.label = peer_info.video_target_receive_bitrate.to_string();
-                video_target_send_bitrate.label = peer_info.video_target_send_bitrate.to_string();
-
-                if (peer_info.video_content_exists) add_video_widgets();
-
-                if (prev_peer_info != null) {
-                        ulong audio_sent_kbps = (peer_info.audio_bytes_sent - prev_peer_info.audio_bytes_sent) * 8 / 1000;
-                        audio_sent_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(audio_sent_kbps);
-                        ulong audio_recv_kbps = (peer_info.audio_bytes_received - prev_peer_info.audio_bytes_received) * 8 / 1000;
-                        audio_recv_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(audio_recv_kbps);
-                        ulong video_sent_kbps = (peer_info.video_bytes_sent - prev_peer_info.video_bytes_sent) * 8 / 1000;
-                        video_sent_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(video_sent_kbps);
-                        ulong video_recv_kbps = (peer_info.video_bytes_received - prev_peer_info.video_bytes_received) * 8 / 1000;
-                        video_recv_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(video_recv_kbps);
-                }
-                prev_peer_info = peer_info;
+            if (peer_info.audio != null) {
+                audio_details.update_content(peer_info.audio);
+            }
+            if (peer_info.video != null) {
+                add_video_widgets();
+                video_details.update_content(peer_info.video);
+            }
         }
 
         private void add_video_widgets() {
-                if (video_added) return;
-
-                grid.attach(new Label("<b>Video</b>") { use_markup=true, xalign=0, visible=true }, 0, row_at++, 1, 1);
-                put_row("RTP");
-                grid.attach(video_rtp_ready, 1, row_at++, 1, 1);
-                put_row("RTCP");
-                grid.attach(video_rtcp_ready, 1, row_at++, 1, 1);
-                put_row("Sent bp/s");
-                grid.attach(video_sent_bps, 1, row_at++, 1, 1);
-                put_row("Received bp/s");
-                grid.attach(video_recv_bps, 1, row_at++, 1, 1);
-                put_row("Codec");
-                grid.attach(video_codec, 1, row_at++, 1, 1);
-                put_row("Target receive bitrate");
-                grid.attach(video_target_receive_bitrate, 1, row_at++, 1, 1);
-                put_row("Target send bitrate");
-                grid.attach(video_target_send_bitrate, 1, row_at++, 1, 1);
-
-                video_added = true;
+            if (video_added) return;
+
+            video_details.visible = true;
+            video_added = true;
+        }
+    }
+
+    public class CallContentDetails : Gtk.Grid {
+
+        public Label rtp_title = new Label("RTP") { xalign=0, visible=true };
+        public Label rtcp_title = new Label("RTCP") { xalign=0, visible=true };
+        public Label target_recv_title = new Label("Target receive bitrate") { xalign=0, visible=true };
+        public Label target_send_title = new Label("Target send bitrate") { xalign=0, visible=true };
+
+        public Label rtp_ready = new Label("?") { xalign=0, visible=true };
+        public Label rtcp_ready = new Label("?") { xalign=0, visible=true };
+        public Label sent_bps = new Label("?") { use_markup=true, xalign=0, visible=true };
+        public Label recv_bps = new Label("?") { use_markup=true, xalign=0, visible=true };
+        public Label codec = new Label("?") { xalign=0, visible=true };
+        public Label target_receive_bitrate = new Label("n/a") { use_markup=true, xalign=0, visible=true };
+        public Label target_send_bitrate = new Label("n/a") { use_markup=true, xalign=0, visible=true };
+
+        private PeerContentInfo? prev_info = null;
+        private int row_at = 0;
+
+        public CallContentDetails(string headline) {
+            attach(new Label("<b>%s</b>".printf(headline)) { use_markup=true, xalign=0, visible=true }, 0, row_at++, 1, 1);
+            attach(rtp_title, 0, row_at, 1, 1);
+            attach(rtp_ready, 1, row_at++, 1, 1);
+            attach(rtcp_title, 0, row_at, 1, 1);
+            attach(rtcp_ready, 1, row_at++, 1, 1);
+            put_row("Sent");
+            attach(sent_bps, 1, row_at++, 1, 1);
+            put_row("Received");
+            attach(recv_bps, 1, row_at++, 1, 1);
+            put_row("Codec");
+            attach(codec, 1, row_at++, 1, 1);
+            attach(target_recv_title, 0, row_at, 1, 1);
+            attach(target_receive_bitrate, 1, row_at++, 1, 1);
+            attach(target_send_title, 0, row_at, 1, 1);
+            attach(target_send_bitrate, 1, row_at++, 1, 1);
+
+            this.column_spacing = 5;
+        }
+
+        public void update_content(PeerContentInfo info) {
+            if (!info.rtp_ready) {
+                rtp_ready.visible = rtcp_ready.visible = true;
+                rtp_title.visible = rtcp_title.visible = true;
+                rtp_ready.label = info.rtp_ready.to_string();
+                rtcp_ready.label = info.rtcp_ready.to_string();
+            } else {
+                rtp_ready.visible = rtcp_ready.visible = false;
+                rtp_title.visible = rtcp_title.visible = false;
+            }
+            if (info.target_send_bytes != -1) {
+                target_receive_bitrate.visible = target_send_bitrate.visible = true;
+                target_recv_title.visible = target_send_title.visible = true;
+                target_receive_bitrate.label = "<span font_family='monospace'>%u</span> kbps".printf(info.target_receive_bytes);
+                target_send_bitrate.label = "<span font_family='monospace'>%u</span> kbps".printf(info.target_send_bytes);
+            } else {
+                target_receive_bitrate.visible = target_send_bitrate.visible = false;
+                target_recv_title.visible = target_send_title.visible = false;
+            }
+
+            codec.label = info.codec + " " + info.clockrate.to_string();
+
+            if (prev_info != null) {
+                ulong audio_sent_kbps = (info.bytes_sent - prev_info.bytes_sent) * 8 / 1000;
+                sent_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(audio_sent_kbps);
+                ulong audio_recv_kbps = (info.bytes_received - prev_info.bytes_received) * 8 / 1000;
+                recv_bps.label = "<span font_family='monospace'>%lu</span> kbps".printf(audio_recv_kbps);
+            }
+            prev_info = info;
+        }
+
+        private void put_row(string label) {
+            attach(new Label(label) { xalign=0, visible=true }, 0, row_at, 1, 1);
         }
     }
 }
diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala
index aad1e6d8..a7d37afd 100644
--- a/main/src/ui/conversation_content_view/call_widget.vala
+++ b/main/src/ui/conversation_content_view/call_widget.vala
@@ -144,7 +144,7 @@ namespace Dino.Ui {
                     });
 
                     break;
-                case Call.State.OTHER_DEVICE_ACCEPTED:
+                case Call.State.OTHER_DEVICE:
                     image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
                     title_label.label = call.direction == Call.DIRECTION_INCOMING ? _("Incoming call") : _("Outgoing call");
                     subtitle_label.label = _("You handled this call on another device");
diff --git a/plugins/rtp/src/stream.vala b/plugins/rtp/src/stream.vala
index 5e5a556b..85864022 100644
--- a/plugins/rtp/src/stream.vala
+++ b/plugins/rtp/src/stream.vala
@@ -99,7 +99,7 @@ public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
             debug("%s.%s probed upstream query %s", pad.get_parent_element().name, pad.name, info.get_query().type.get_name());
         }
         if ((info.type & Gst.PadProbeType.BUFFER) > 0) {
-            uint id = pad.get_data("no_buffer_probe_timeout");
+            uint id = pad.steal_data("no_buffer_probe_timeout");
             if (id != 0) {
                 Source.remove(id);
             }
diff --git a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala
index 9022547d..c4c299c5 100644
--- a/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala
+++ b/xmpp-vala/src/module/xep/0167_jingle_rtp/content_parameters.vala
@@ -151,6 +151,7 @@ public class Xmpp.Xep.JingleRtp.Parameters : Jingle.ContentParameters, Object {
         }
 
         this.stream = parent.create_stream(content);
+        this.stream.weak_ref(() => this.stream = null);
         rtp_datagram.datagram_received.connect(this.stream.on_recv_rtp_data);
         rtcp_datagram.datagram_received.connect(this.stream.on_recv_rtcp_data);
         this.stream.on_send_rtp_data.connect(rtp_datagram.send_datagram);
diff --git a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala
index 71e16a95..ac1d8329 100644
--- a/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala
+++ b/xmpp-vala/src/module/xep/0353_jingle_message_initiation.vala
@@ -8,7 +8,7 @@ namespace Xmpp.Xep.JingleMessageInitiation {
 
         public signal void session_proposed(Jid from, Jid to, string sid, Gee.List<StanzaNode> descriptions);
         public signal void session_retracted(Jid from, Jid to, string sid);
-        public signal void session_accepted(Jid from, string sid);
+        public signal void session_accepted(Jid from, Jid to, string sid);
         public signal void session_rejected(Jid from, Jid to, string sid);
 
         public void send_session_propose_to_peer(XmppStream stream, Jid to, string sid, Gee.List<StanzaNode> descriptions) {
@@ -65,7 +65,7 @@ namespace Xmpp.Xep.JingleMessageInitiation {
             switch (mi_node.name) {
                 case "accept":
                 case "proceed":
-                    session_accepted(message.from, mi_node.get_attribute("id"));
+                    session_accepted(message.from, message.to, mi_node.get_attribute("id"));
                     break;
                 case "propose":
                     ArrayList<StanzaNode> descriptions = new ArrayList<StanzaNode>();
-- 
cgit v1.2.3-70-g09d2


From 78bb2bbddaf587de77f67c404e8ed5083f16bf8a Mon Sep 17 00:00:00 2001
From: fiaxh <git@lightrise.org>
Date: Sat, 18 Dec 2021 21:34:39 +0100
Subject: Add calls in private MUCs via a MUJI MUC

---
 libdino/src/entity/call.vala                      | 13 +++--
 libdino/src/service/call_state.vala               | 39 +++++++++++++-
 libdino/src/service/calls.vala                    | 62 +++++++++++++++++------
 main/src/ui/conversation_titlebar/call_entry.vala | 18 +++----
 xmpp-vala/src/module/xep/muji_meta.vala           | 36 ++++++-------
 5 files changed, 116 insertions(+), 52 deletions(-)

(limited to 'libdino/src')

diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala
index 5221a807..3b48f664 100644
--- a/libdino/src/entity/call.vala
+++ b/libdino/src/entity/call.vala
@@ -39,11 +39,6 @@ namespace Dino.Entities {
             id = row[db.call.id];
             account = db.get_account_by_id(row[db.call.account_id]);
 
-            counterpart = db.get_jid_by_id(row[db.call.counterpart_id]);
-            string counterpart_resource = row[db.call.counterpart_resource];
-            if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
-            counterparts.add(counterpart);
-
             string our_resource = row[db.call.our_resource];
             if (our_resource != null) {
                 ourpart = account.bare_jid.with_resource(our_resource);
@@ -66,6 +61,14 @@ namespace Dino.Entities {
                 if (counterpart == null) counterpart = peer;
             }
 
+            counterpart = db.get_jid_by_id(row[db.call.counterpart_id]);
+            string counterpart_resource = row[db.call.counterpart_resource];
+            if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
+            if (counterparts.is_empty) {
+                counterparts.add(counterpart);
+                counterpart = counterpart;
+            }
+
             notify.connect(on_update);
         }
 
diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala
index 385cf9dc..51563552 100644
--- a/libdino/src/service/call_state.vala
+++ b/libdino/src/service/call_state.vala
@@ -11,6 +11,7 @@ public class Dino.CallState : Object {
     public StreamInteractor stream_interactor;
     public Call call;
     public Xep.Muji.GroupCall? group_call { get; set; }
+    public Jid? parent_muc { get; set; }
     public Jid? invited_to_group_call = null;
     public Jid? group_call_inviter = null;
     public bool accepted { get; private set; default=false; }
@@ -19,6 +20,8 @@ public class Dino.CallState : Object {
     public bool we_should_send_video { get; set; default=false; }
     public HashMap<Jid, PeerState> peers = new HashMap<Jid, PeerState>(Jid.hash_func, Jid.equals_func);
 
+    private string message_type = Xmpp.MessageStanza.TYPE_CHAT;
+
     public CallState(Call call, StreamInteractor stream_interactor) {
         this.call = call;
         this.stream_interactor = stream_interactor;
@@ -37,6 +40,29 @@ public class Dino.CallState : Object {
         }
     }
 
+    internal async void initiate_groupchat_call(Jid muc) {
+        parent_muc = muc;
+        message_type = Xmpp.MessageStanza.TYPE_GROUPCHAT;
+
+        if (this.group_call == null) yield convert_into_group_call();
+        if (this.group_call == null) return;
+        // The user might have retracted the call in the meanwhile
+        if (this.call.state != Call.State.RINGING) return;
+
+        XmppStream stream = stream_interactor.get_stream(call.account);
+        if (stream == null) return;
+
+        Gee.List<Jid> occupants = stream_interactor.get_module(MucManager.IDENTITY).get_other_occupants(muc, call.account);
+        foreach (Jid occupant in occupants) {
+            Jid? real_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(occupant, call.account);
+            if (real_jid == null) continue;
+            debug(@"Adding MUC member as MUJI MUC owner %s", real_jid.bare_jid.to_string());
+            yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, real_jid.bare_jid, null, "owner");
+        }
+
+        stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, muc, group_call.muc_jid, we_should_send_video, message_type);
+    }
+
     internal PeerState set_first_peer(Jid peer) {
         var peer_state = new PeerState(peer, call, stream_interactor);
         peer_state.first_peer = true;
@@ -57,7 +83,7 @@ public class Dino.CallState : Object {
         if (invited_to_group_call != null) {
             XmppStream stream = stream_interactor.get_stream(call.account);
             if (stream == null) return;
-            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_accept_to_peer(stream, group_call_inviter, invited_to_group_call);
+            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_accept_to_peer(stream, group_call_inviter, invited_to_group_call, message_type);
             join_group_call.begin(invited_to_group_call);
         } else {
             foreach (PeerState peer in peers.values) {
@@ -73,7 +99,7 @@ public class Dino.CallState : Object {
             XmppStream stream = stream_interactor.get_stream(call.account);
             if (stream == null) return;
             stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_self(stream, invited_to_group_call);
-            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_peer(stream, group_call_inviter, invited_to_group_call);
+            stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_reject_to_peer(stream, group_call_inviter, invited_to_group_call, message_type);
         }
         var peers_cpy = new ArrayList<PeerState>();
         peers_cpy.add_all(peers.values);
@@ -87,6 +113,10 @@ public class Dino.CallState : Object {
         var peers_cpy = new ArrayList<PeerState>();
         peers_cpy.add_all(peers.values);
 
+        if (group_call != null) {
+            stream_interactor.get_module(MucManager.IDENTITY).part(call.account, group_call.muc_jid);
+        }
+
         if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
             foreach (PeerState peer in peers_cpy) {
                 peer.end(Xep.Jingle.ReasonElement.SUCCESS);
@@ -96,6 +126,11 @@ public class Dino.CallState : Object {
             foreach (PeerState peer in peers_cpy) {
                 peer.end(Xep.Jingle.ReasonElement.CANCEL);
             }
+            if (parent_muc != null) {
+                XmppStream stream = stream_interactor.get_stream(call.account);
+                if (stream == null) return;
+                stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, parent_muc, group_call.muc_jid, message_type);
+            }
             call.state = Call.State.MISSED;
         } else {
             return;
diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala
index 3d1ed7e8..629d8074 100644
--- a/libdino/src/service/calls.vala
+++ b/libdino/src/service/calls.vala
@@ -39,7 +39,8 @@ namespace Dino {
             Call call = new Call();
             call.direction = Call.DIRECTION_OUTGOING;
             call.account = conversation.account;
-            call.add_peer(conversation.counterpart);
+            // TODO we should only do that for Conversation.Type.CHAT, but the database currently requires a counterpart from the start
+            call.counterpart = conversation.counterpart;
             call.ourpart = conversation.account.full_jid;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
             call.state = Call.State.RINGING;
@@ -50,9 +51,14 @@ namespace Dino {
             call_state.we_should_send_video = video;
             call_state.we_should_send_audio = true;
             connect_call_state_signals(call_state);
-            PeerState peer_state = call_state.set_first_peer(conversation.counterpart);
 
-            yield peer_state.initiate_call(conversation.counterpart);
+            if (conversation.type_ == Conversation.Type.CHAT) {
+                call.add_peer(conversation.counterpart);
+                PeerState peer_state = call_state.set_first_peer(conversation.counterpart);
+                yield peer_state.initiate_call(conversation.counterpart);
+            } else {
+                call_state.initiate_groupchat_call(conversation.counterpart);
+            }
 
             conversation.last_active = call.time;
 
@@ -63,7 +69,12 @@ namespace Dino {
 
         public async bool can_do_audio_calls_async(Conversation conversation) {
             if (!can_do_audio_calls()) return false;
-            return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
+
+            if (conversation.type_ == Conversation.Type.CHAT) {
+                return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
+            } else {
+                return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
+            }
         }
 
         private bool can_do_audio_calls() {
@@ -75,7 +86,12 @@ namespace Dino {
 
         public async bool can_do_video_calls_async(Conversation conversation) {
             if (!can_do_video_calls()) return false;
-            return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
+
+            if (conversation.type_ == Conversation.Type.CHAT) {
+                return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
+            } else {
+                return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
+            }
         }
 
         private bool can_do_video_calls() {
@@ -237,17 +253,24 @@ namespace Dino {
 
         private CallState? get_call_state_for_groupcall(Account account, Jid muc_jid) {
             foreach (CallState call_state in call_states.values) {
-                if (call_state.group_call != null && call_state.call.account.equals(account) && call_state.group_call.muc_jid.equals(muc_jid)) {
-                    return call_state;
-                }
+                if (!call_state.call.account.equals(account)) continue;
+
+                if (call_state.group_call != null && call_state.group_call.muc_jid.equals(muc_jid)) return call_state;
+                if (call_state.invited_to_group_call != null && call_state.invited_to_group_call.equals(muc_jid)) return call_state;
             }
             return null;
         }
 
-        private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, Gee.List<StanzaNode> descriptions) {
-            debug("[%s] on_muji_call_received", account.bare_jid.to_string());
+        private async void on_muji_call_received(Account account, Jid inviter_jid, Jid muc_jid, Gee.List<StanzaNode> descriptions, string message_type) {
+            debug("[%s] Muji call received from %s for MUC %s, type %s", account.bare_jid.to_string(), inviter_jid.to_string(), muc_jid.to_string(), message_type);
+
             foreach (Call call in call_states.keys) {
-                if (call.account.equals(account) && call.counterparts.contains(inviter_jid) && call_states[call].accepted) {
+                if (!call.account.equals(account)) return;
+
+                // We already know the call; this is a reflection of our own invite
+                if (call_states[call].parent_muc.equals_bare(inviter_jid)) return;
+
+                if (call.counterparts.contains(inviter_jid) && call_states[call].accepted) {
                     // A call is converted into a group call.
                     yield call_states[call].join_group_call(muc_jid);
                     return;
@@ -373,11 +396,11 @@ namespace Dino {
             });
 
             Xep.MujiMeta.Module muji_meta_module = stream_interactor.module_manager.get_module(account, Xep.MujiMeta.Module.IDENTITY);
-            muji_meta_module.call_proposed.connect((inviter_jid, to, muc_jid, descriptions) => {
+            muji_meta_module.call_proposed.connect((inviter_jid, to, muc_jid, descriptions, message_type) => {
                 if (inviter_jid.equals_bare(account.bare_jid)) return;
-                on_muji_call_received.begin(account, inviter_jid, muc_jid, descriptions);
+                on_muji_call_received.begin(account, inviter_jid, muc_jid, descriptions, message_type);
             });
-            muji_meta_module.call_accepted.connect((from_jid, muc_jid) => {
+            muji_meta_module.call_accepted.connect((from_jid, muc_jid, message_type) => {
                 if (!from_jid.equals_bare(account.bare_jid)) return;
 
                 // We accepted the call from another device
@@ -387,17 +410,24 @@ namespace Dino {
                 call_state.call.state = Call.State.OTHER_DEVICE;
                 remove_call_from_datastructures(call_state.call);
             });
-            muji_meta_module.call_retracted.connect((from_jid, muc_jid) => {
+            muji_meta_module.call_retracted.connect((from_jid, to_jid, muc_jid, message_type) => {
                 if (from_jid.equals_bare(account.bare_jid)) return;
 
                 // The call was retracted by the counterpart
                 CallState? call_state = get_call_state_for_groupcall(account, muc_jid);
                 if (call_state == null) return;
 
+                if (call_state.call.state != Call.State.RINGING) {
+                    debug("%s tried to retract a call that's in state %s. Ignoring.", from_jid.to_string(), call_state.call.state.to_string());
+                    return;
+                }
+
+                // TODO prevent other MUC occupants from retracting a call
+
                 call_state.call.state = Call.State.MISSED;
                 remove_call_from_datastructures(call_state.call);
             });
-            muji_meta_module.call_rejected.connect((from_jid, to_jid, muc_jid) => {
+            muji_meta_module.call_rejected.connect((from_jid, to_jid, muc_jid, message_type) => {
                 if (from_jid.equals_bare(account.bare_jid)) return;
                 debug(@"[%s] rejected our MUJI invite to %s", account.bare_jid.to_string(), from_jid.to_string(), muc_jid.to_string());
             });
diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala
index 3b3a5b39..3fa399a6 100644
--- a/main/src/ui/conversation_titlebar/call_entry.vala
+++ b/main/src/ui/conversation_titlebar/call_entry.vala
@@ -115,17 +115,13 @@ namespace Dino.Ui {
                 return;
             }
 
-            if (conversation.type_ == Conversation.Type.CHAT) {
-                Conversation conv_bak = conversation;
-                bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
-                bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
-                if (conv_bak != conversation) return;
-
-                visible = audio_works;
-                video_button.visible = video_works;
-            } else {
-                visible = false;
-            }
+            Conversation conv_bak = conversation;
+            bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
+            bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
+            if (conv_bak != conversation) return;
+
+            visible = audio_works;
+            video_button.visible = video_works;
         }
 
         public new void unset_conversation() { }
diff --git a/xmpp-vala/src/module/xep/muji_meta.vala b/xmpp-vala/src/module/xep/muji_meta.vala
index 4452e611..89a0e8de 100644
--- a/xmpp-vala/src/module/xep/muji_meta.vala
+++ b/xmpp-vala/src/module/xep/muji_meta.vala
@@ -6,48 +6,48 @@ namespace Xmpp.Xep.MujiMeta {
     public class Module : XmppStreamModule {
         public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "muji_meta");
 
-        public signal void call_proposed(Jid from, Jid to, Jid muc_jid, Gee.List<StanzaNode> descriptions);
-        public signal void call_retracted(Jid from, Jid to, Jid muc_jid);
-        public signal void call_accepted(Jid from, Jid muc_jid);
-        public signal void call_rejected(Jid from, Jid to, Jid muc_jid);
+        public signal void call_proposed(Jid from, Jid to, Jid muc_jid, Gee.List<StanzaNode> descriptions, string message_type);
+        public signal void call_retracted(Jid from, Jid to, Jid muc_jid, string message_type);
+        public signal void call_accepted(Jid from, Jid muc_jid, string message_type);
+        public signal void call_rejected(Jid from, Jid to, Jid muc_jid, string message_type);
 
-        public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video) {
+        public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video, string? message_type = null) {
             var invite_node = new StanzaNode.build("propose", NS_URI).put_attribute("muc", muc_jid.to_string());
             invite_node.put_node(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
             if (video) {
                 invite_node.put_node(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "video"));
             }
             var muji_node = new StanzaNode.build("muji", NS_URI).add_self_xmlns().put_node(invite_node);
-            MessageStanza invite_message = new MessageStanza() { to=invitee, type_=MessageStanza.TYPE_CHAT };
+            MessageStanza invite_message = new MessageStanza() { to=invitee, type_=message_type };
             invite_message.stanza.put_node(muji_node);
             stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, invite_message);
         }
 
-        public void send_invite_retract_to_peer(XmppStream stream, Jid invitee, Jid muc_jid) {
-            send_jmi_message(stream, "retract", invitee, muc_jid);
+        public void send_invite_retract_to_peer(XmppStream stream, Jid invitee, Jid muc_jid, string? message_type = null) {
+            send_jmi_message(stream, "retract", invitee, muc_jid, message_type);
         }
 
-        public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid) {
-            send_jmi_message(stream, "accept", invitor, muc_jid);
+        public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string? message_type = null) {
+            send_jmi_message(stream, "accept", invitor, muc_jid, message_type);
         }
 
         public void send_invite_accept_to_self(XmppStream stream, Jid muc_jid) {
             send_jmi_message(stream, "accept", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid);
         }
 
-        public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid) {
-            send_jmi_message(stream, "reject", invitor, muc_jid);
+        public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string? message_type = null) {
+            send_jmi_message(stream, "reject", invitor, muc_jid, message_type);
         }
 
         public void send_invite_reject_to_self(XmppStream stream, Jid muc_jid) {
             send_jmi_message(stream, "reject", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid);
         }
 
-        private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc) {
+        private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc, string? message_type = null) {
             var jmi_node = new StanzaNode.build(name, NS_URI).add_self_xmlns().put_attribute("muc", muc.to_string());
             var muji_node = new StanzaNode.build("muji", NS_URI).add_self_xmlns().put_node(jmi_node);
 
-            MessageStanza accepted_message = new MessageStanza() { to=to, type_=MessageStanza.TYPE_CHAT };
+            MessageStanza accepted_message = new MessageStanza() { to=to, type_= message_type ?? MessageStanza.TYPE_CHAT };
             accepted_message.stanza.put_node(muji_node);
             stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, accepted_message);
         }
@@ -80,7 +80,7 @@ namespace Xmpp.Xep.MujiMeta {
             switch (mi_node.name) {
                 case "accept":
                 case "proceed":
-                    call_accepted(message.from, muc_jid);
+                    call_accepted(message.from, muc_jid, message.type_);
                     break;
                 case "propose":
                     ArrayList<StanzaNode> descriptions = new ArrayList<StanzaNode>();
@@ -91,14 +91,14 @@ namespace Xmpp.Xep.MujiMeta {
                     }
 
                     if (descriptions.size > 0) {
-                        call_proposed(message.from, message.to, muc_jid, descriptions);
+                        call_proposed(message.from, message.to, muc_jid, descriptions, message.type_);
                     }
                     break;
                 case "retract":
-                    call_retracted(message.from, message.to, muc_jid);
+                    call_retracted(message.from, message.to, muc_jid, message.type_);
                     break;
                 case "reject":
-                    call_rejected(message.from, message.to, muc_jid);
+                    call_rejected(message.from, message.to, muc_jid, message.type_);
                     break;
             }
         }
-- 
cgit v1.2.3-70-g09d2


From f0c7dd0682fec8d72c644d8e54896de7bdc40ddb Mon Sep 17 00:00:00 2001
From: fiaxh <git@lightrise.org>
Date: Mon, 20 Dec 2021 00:15:05 +0100
Subject: UI + libdino: Improve MUJI calls from MUC

- Move calls from ICE-thead onto main thread
- Identify Call.ourpart as MUC nick if in MUC
- Keep track of the initiator of a call
---
 libdino/src/entity/call.vala                       |  9 ++++----
 libdino/src/service/call_peer_state.vala           | 14 +++++++++----
 libdino/src/service/call_state.vala                |  9 ++++++--
 libdino/src/service/call_store.vala                |  9 +++++---
 libdino/src/service/calls.vala                     | 24 ++++++++++++----------
 libdino/src/service/content_item_store.vala        |  4 ++--
 libdino/src/service/muc_manager.vala               |  2 +-
 main/data/call_widget.ui                           |  2 +-
 main/src/ui/application.vala                       | 18 ++++++++++++----
 main/src/ui/call_window/call_window.vala           |  2 +-
 .../ui/conversation_content_view/call_widget.vala  |  2 +-
 main/src/ui/conversation_titlebar/call_entry.vala  | 18 +++++++++-------
 main/src/ui/notifier_freedesktop.vala              |  6 ++++--
 13 files changed, 75 insertions(+), 44 deletions(-)

(limited to 'libdino/src')

diff --git a/libdino/src/entity/call.vala b/libdino/src/entity/call.vala
index 3b48f664..8e5bc246 100644
--- a/libdino/src/entity/call.vala
+++ b/libdino/src/entity/call.vala
@@ -20,9 +20,12 @@ namespace Dino.Entities {
 
         public int id { get; set; default=-1; }
         public Account account { get; set; }
-        public Jid counterpart { get; set; } // For backwards compatibility with db version 21. Not to be used anymore.
+        public Jid counterpart { get; set; }
         public Gee.List<Jid> counterparts = new Gee.ArrayList<Jid>(Jid.equals_bare_func);
         public Jid ourpart { get; set; }
+        public Jid proposer {
+            get { return direction == DIRECTION_OUTGOING ? ourpart : counterpart; }
+        }
         public bool direction { get; set; }
         public DateTime time { get; set; }
         public DateTime local_time { get; set; }
@@ -58,7 +61,6 @@ namespace Dino.Entities {
                 if (!counterparts.contains(peer)) { // Legacy: The first peer is also in the `call` table. Don't add twice.
                     counterparts.add(peer);
                 }
-                if (counterpart == null) counterpart = peer;
             }
 
             counterpart = db.get_jid_by_id(row[db.call.counterpart_id]);
@@ -66,7 +68,6 @@ namespace Dino.Entities {
             if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
             if (counterparts.is_empty) {
                 counterparts.add(counterpart);
-                counterpart = counterpart;
             }
 
             notify.connect(on_update);
@@ -107,8 +108,6 @@ namespace Dino.Entities {
         }
 
         public void add_peer(Jid peer) {
-            if (counterpart == null) counterpart = peer;
-
             if (counterparts.contains(peer)) return;
 
             counterparts.add(peer);
diff --git a/libdino/src/service/call_peer_state.vala b/libdino/src/service/call_peer_state.vala
index 8c4b0930..09440371 100644
--- a/libdino/src/service/call_peer_state.vala
+++ b/libdino/src/service/call_peer_state.vala
@@ -127,8 +127,6 @@ public class Dino.PeerState : Object {
     }
 
     public void reject() {
-        call.state = Call.State.DECLINED;
-
         if (session != null) {
             foreach (Xep.Jingle.Content content in session.contents) {
                 content.reject();
@@ -299,7 +297,12 @@ public class Dino.PeerState : Object {
 
         debug(@"[%s] %s connecting content signals %s", call.account.bare_jid.to_string(), jid.to_string(), rtp_content_parameter.media);
         rtp_content_parameter.stream_created.connect((stream) => on_stream_created(rtp_content_parameter.media, stream));
-        rtp_content_parameter.connection_ready.connect((status) => on_connection_ready(content, rtp_content_parameter.media));
+        rtp_content_parameter.connection_ready.connect((status) => {
+            Idle.add(() => {
+                on_connection_ready(content, rtp_content_parameter.media);
+                return false;
+            });
+        });
 
         content.senders_modify_incoming.connect((content, proposed_senders) => {
             if (content.session.senders_include_us(content.senders) != content.session.senders_include_us(proposed_senders)) {
@@ -342,7 +345,10 @@ public class Dino.PeerState : Object {
         if (media == "video" && stream.receiving) {
             counterpart_sends_video = true;
             video_content_parameter.connection_ready.connect((status) => {
-                counterpart_sends_video_updated(false);
+                Idle.add(() => {
+                    counterpart_sends_video_updated(false);
+                    return false;
+                });
             });
         }
 
diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala
index 51563552..188a8321 100644
--- a/libdino/src/service/call_state.vala
+++ b/libdino/src/service/call_state.vala
@@ -126,7 +126,7 @@ public class Dino.CallState : Object {
             foreach (PeerState peer in peers_cpy) {
                 peer.end(Xep.Jingle.ReasonElement.CANCEL);
             }
-            if (parent_muc != null) {
+            if (parent_muc != null && group_call != null) {
                 XmppStream stream = stream_interactor.get_stream(call.account);
                 if (stream == null) return;
                 stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, parent_muc, group_call.muc_jid, message_type);
@@ -242,7 +242,12 @@ public class Dino.CallState : Object {
         XmppStream stream = stream_interactor.get_stream(call.account);
         if (stream == null) return;
 
-        Jid muc_jid = stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[call.account] ?? new Jid("chat.jabberfr.org");
+        Jid? muc_jid = null;
+        if (muc_jid == null) {
+            warning("Failed to initiate group call: MUC server not known.");
+            return;
+        }
+
         muc_jid = new Jid("%08x@".printf(Random.next_int()) + muc_jid.to_string()); // TODO longer?
 
         debug("[%s] Converting call to groupcall %s", call.account.bare_jid.to_string(), muc_jid.to_string());
diff --git a/libdino/src/service/call_store.vala b/libdino/src/service/call_store.vala
index fa6e63ee..bfc8255f 100644
--- a/libdino/src/service/call_store.vala
+++ b/libdino/src/service/call_store.vala
@@ -30,7 +30,7 @@ namespace Dino {
             cache_call(call);
         }
 
-        public Call? get_call_by_id(int id) {
+        public Call? get_call_by_id(int id, Conversation conversation) {
             Call? call = calls_by_db_id[id];
             if (call != null) {
                 return call;
@@ -38,14 +38,17 @@ namespace Dino {
 
             RowOption row_option = db.call.select().with(db.call.id, "=", id).row();
 
-            return create_call_from_row_opt(row_option);
+            return create_call_from_row_opt(row_option, conversation);
         }
 
-        private Call? create_call_from_row_opt(RowOption row_opt) {
+        private Call? create_call_from_row_opt(RowOption row_opt, Conversation conversation) {
             if (!row_opt.is_present()) return null;
 
             try {
                 Call call = new Call.from_row(db, row_opt.inner);
+                if (conversation.type_.is_muc_semantic()) {
+                    call.ourpart = conversation.counterpart.with_resource(call.ourpart.resourcepart);
+                }
                 cache_call(call);
                 return call;
             } catch (InvalidJidError e) {
diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala
index 629d8074..3e2181a5 100644
--- a/libdino/src/service/calls.vala
+++ b/libdino/src/service/calls.vala
@@ -39,9 +39,8 @@ namespace Dino {
             Call call = new Call();
             call.direction = Call.DIRECTION_OUTGOING;
             call.account = conversation.account;
-            // TODO we should only do that for Conversation.Type.CHAT, but the database currently requires a counterpart from the start
             call.counterpart = conversation.counterpart;
-            call.ourpart = conversation.account.full_jid;
+            call.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.full_jid;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
             call.state = Call.State.RINGING;
 
@@ -57,7 +56,7 @@ namespace Dino {
                 PeerState peer_state = call_state.set_first_peer(conversation.counterpart);
                 yield peer_state.initiate_call(conversation.counterpart);
             } else {
-                call_state.initiate_groupchat_call(conversation.counterpart);
+                call_state.initiate_groupchat_call.begin(conversation.counterpart);
             }
 
             conversation.last_active = call.time;
@@ -213,24 +212,23 @@ namespace Dino {
 
         private PeerState create_received_call(Account account, Jid from, Jid to, bool video_requested) {
             Call call = new Call();
-            Jid counterpart = null;
             if (from.equals_bare(account.bare_jid)) {
                 // Call requested by another of our devices
                 call.direction = Call.DIRECTION_OUTGOING;
                 call.ourpart = from;
                 call.state = Call.State.OTHER_DEVICE;
-                counterpart = to;
+                call.counterpart = to;
             } else {
                 call.direction = Call.DIRECTION_INCOMING;
                 call.ourpart = account.full_jid;
                 call.state = Call.State.RINGING;
-                counterpart = from;
+                call.counterpart = from;
             }
-            call.add_peer(counterpart);
+            call.add_peer(call.counterpart);
             call.account = account;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
 
-            Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(counterpart.bare_jid, account, Conversation.Type.CHAT);
+            Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT);
 
             stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
 
@@ -238,7 +236,7 @@ namespace Dino {
 
             var call_state = new CallState(call, stream_interactor);
             connect_call_state_signals(call_state);
-            PeerState peer_state = call_state.set_first_peer(counterpart);
+            PeerState peer_state = call_state.set_first_peer(call.counterpart);
             call_state.we_should_send_video = video_requested;
             call_state.we_should_send_audio = true;
 
@@ -283,7 +281,7 @@ namespace Dino {
             Call call = new Call();
             call.direction = Call.DIRECTION_INCOMING;
             call.ourpart = account.full_jid;
-            call.add_peer(inviter_jid); // not rly
+            call.counterpart = inviter_jid;
             call.account = account;
             call.time = call.local_time = call.end_time = new DateTime.now_utc();
             call.state = Call.State.RINGING;
@@ -361,13 +359,14 @@ namespace Dino {
                     if (from.equals(account.full_jid)) return;
 
                     Call call = current_jmi_request_peer[account].call;
+                    call.ourpart = from;
                     call.state = Call.State.OTHER_DEVICE;
                     remove_call_from_datastructures(call);
                 } else if (from.equals_bare(current_jmi_request_peer[account].jid) && to.equals(account.full_jid)) { // Message from our peer
                     // We proposed the call
                     // We know the full jid of our peer now
                     current_jmi_request_call[account].rename_peer(current_jmi_request_peer[account].jid, from);
-                    current_jmi_request_peer[account].call_resource(from);
+                    current_jmi_request_peer[account].call_resource.begin(from);
                 }
             });
             mi_module.session_rejected.connect((from, to, sid) => {
@@ -378,6 +377,9 @@ namespace Dino {
                 bool incoming_reject = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(account.bare_jid);
                 if (!outgoing_reject && !incoming_reject) return;
 
+                // We don't care if a single person in a group call rejected the call
+                if (incoming_reject && call_states[call].group_call != null) return;
+
                 call.state = Call.State.DECLINED;
                 call_states[call].terminated(from, Xep.Jingle.ReasonElement.DECLINE, "JMI reject");
                 remove_call_from_datastructures(call);
diff --git a/libdino/src/service/content_item_store.vala b/libdino/src/service/content_item_store.vala
index e0102d24..7a8e38b8 100644
--- a/libdino/src/service/content_item_store.vala
+++ b/libdino/src/service/content_item_store.vala
@@ -69,7 +69,7 @@ public class ContentItemStore : StreamInteractionModule, Object {
                     }
                     break;
                 case 3:
-                    Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(foreign_id);
+                    Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(foreign_id, conversation);
                     if (call != null) {
                         var call_item = new CallItem(call, conversation, row[db.content_item.id]);
                         items.add(call_item);
@@ -321,7 +321,7 @@ public class CallItem : ContentItem {
     public Conversation conversation;
 
     public CallItem(Call call, Conversation conversation, int id) {
-        base(id, TYPE, call.direction == Call.DIRECTION_OUTGOING ? conversation.account.bare_jid : conversation.counterpart, call.time, call.encryption, Message.Marked.NONE);
+        base(id, TYPE, call.proposer, call.time, call.encryption, Message.Marked.NONE);
 
         this.call = call;
         this.conversation = conversation;
diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala
index 8a317a08..05473647 100644
--- a/libdino/src/service/muc_manager.vala
+++ b/libdino/src/service/muc_manager.vala
@@ -424,7 +424,7 @@ public class MucManager : StreamInteractionModule, Object {
                 foreach (Xep.ServiceDiscovery.Identity identity in identities) {
                     if (identity.category == Xep.ServiceDiscovery.Identity.CATEGORY_CONFERENCE) {
                         default_muc_server[account] = item.jid;
-                        print(@"$(account.bare_jid) Default MUC: $(item.jid)\n");
+                        debug("[%s] Default MUC: %s", account.bare_jid.to_string(), item.jid.to_string());
                         return;
                     }
                 }
diff --git a/main/data/call_widget.ui b/main/data/call_widget.ui
index 8e2ee36c..0c5d8bfa 100644
--- a/main/data/call_widget.ui
+++ b/main/data/call_widget.ui
@@ -98,7 +98,7 @@
                                         <child>
                                             <object class="GtkBox" id="multiparty_peer_box">
                                                 <property name="margin">10</property>
-                                                <property name="spacing">5</property>
+                                                <property name="spacing">7</property>
                                                 <property name="hexpand">True</property>
                                             </object>
                                         </child>
diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala
index 9f48caec..ecbea85e 100644
--- a/main/src/ui/application.vala
+++ b/main/src/ui/application.vala
@@ -206,9 +206,14 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
         });
         add_action(open_shortcuts_action);
 
-        SimpleAction accept_call_action = new SimpleAction("accept-call", VariantType.INT32);
+        SimpleAction accept_call_action = new SimpleAction("accept-call", new VariantType.tuple(new VariantType[]{VariantType.INT32, VariantType.INT32}));
         accept_call_action.activate.connect((variant) => {
-            Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32());
+            int conversation_id = variant.get_child_value(0).get_int32();
+            Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(conversation_id);
+            if (conversation == null) return;
+
+            int call_id = variant.get_child_value(1).get_int32();
+            Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(call_id, conversation);
             CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call];
             if (call_state == null) return;
 
@@ -220,9 +225,14 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
         });
         add_action(accept_call_action);
 
-        SimpleAction deny_call_action = new SimpleAction("reject-call", VariantType.INT32);
+        SimpleAction deny_call_action = new SimpleAction("reject-call", new VariantType.tuple(new VariantType[]{VariantType.INT32, VariantType.INT32}));
         deny_call_action.activate.connect((variant) => {
-            Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32());
+            int conversation_id = variant.get_child_value(0).get_int32();
+            Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_by_id(conversation_id);
+            if (conversation == null) return;
+
+            int call_id = variant.get_child_value(1).get_int32();
+            Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(call_id, conversation);
             CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call];
             if (call_state == null) return;
 
diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala
index fab424bf..c610444f 100644
--- a/main/src/ui/call_window/call_window.vala
+++ b/main/src/ui/call_window/call_window.vala
@@ -20,7 +20,7 @@ namespace Dino.Ui {
         public Revealer header_bar_revealer = new Revealer() { halign=Align.END, valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
         public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { halign=Align.END, valign=Align.END, visible=true };
         public Revealer invite_button_revealer = new Revealer() { margin_top=50, margin_right=30, halign=Align.END, valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200 };
-        public Button invite_button = new Button.from_icon_name("dino-account-plus") { relief=ReliefStyle.NONE };
+        public Button invite_button = new Button.from_icon_name("dino-account-plus") { relief=ReliefStyle.NONE, visible=false };
         private Widget? own_video = null;
         private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
         private ArrayList<string> participants = new ArrayList<string>();
diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala
index a7d37afd..a2c8c0c2 100644
--- a/main/src/ui/conversation_content_view/call_widget.vala
+++ b/main/src/ui/conversation_content_view/call_widget.vala
@@ -86,7 +86,7 @@ namespace Dino.Ui {
 
         private void update_counterparts() {
             if (call.state != Call.State.IN_PROGRESS && call.state != Call.State.ENDED) return;
-            if (call.counterparts.size <= 1) return;
+            if (call.counterparts.size <= 1 && conversation.type_ == Conversation.Type.CHAT) return;
 
             multiparty_peer_box.foreach((widget) => { multiparty_peer_box.remove(widget); });
 
diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala
index 3fa399a6..3b3a5b39 100644
--- a/main/src/ui/conversation_titlebar/call_entry.vala
+++ b/main/src/ui/conversation_titlebar/call_entry.vala
@@ -115,13 +115,17 @@ namespace Dino.Ui {
                 return;
             }
 
-            Conversation conv_bak = conversation;
-            bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
-            bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
-            if (conv_bak != conversation) return;
-
-            visible = audio_works;
-            video_button.visible = video_works;
+            if (conversation.type_ == Conversation.Type.CHAT) {
+                Conversation conv_bak = conversation;
+                bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
+                bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
+                if (conv_bak != conversation) return;
+
+                visible = audio_works;
+                video_button.visible = video_works;
+            } else {
+                visible = false;
+            }
         }
 
         public new void unset_conversation() { }
diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala
index 83e9bf57..e8e2ba1d 100644
--- a/main/src/ui/notifier_freedesktop.vala
+++ b/main/src/ui/notifier_freedesktop.vala
@@ -141,10 +141,12 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
                 GLib.Application.get_default().activate_action("open-conversation", new Variant.int32(conversation.id));
             });
             add_action_listener(notification_id, "reject", () => {
-                GLib.Application.get_default().activate_action("reject-call", new Variant.int32(call.id));
+                var variant = new Variant.tuple(new Variant[] {new Variant.int32(conversation.id), new Variant.int32(call.id)});
+                GLib.Application.get_default().activate_action("reject-call", variant);
             });
             add_action_listener(notification_id, "accept", () => {
-                GLib.Application.get_default().activate_action("accept-call", new Variant.int32(call.id));
+                var variant = new Variant.tuple(new Variant[] {new Variant.int32(conversation.id), new Variant.int32(call.id)});
+                GLib.Application.get_default().activate_action("accept-call", variant);
             });
         } catch (Error e) {
             warning("Failed showing subscription request notification: %s", e.message);
-- 
cgit v1.2.3-70-g09d2