diff options
author | fiaxh <git@lightrise.org> | 2021-11-04 17:33:08 +0100 |
---|---|---|
committer | fiaxh <git@lightrise.org> | 2021-11-10 11:05:34 +0100 |
commit | 26d10d1dcb95f11b65611473c9840e13683cb5ec (patch) | |
tree | fb09e36aba28ff1b971ea11ad3da2dd3d92f31d4 /libdino/src/service/call_state.vala | |
parent | 38944d702331bb9d7a91d8ed05a33c935562e3c0 (diff) | |
download | dino-26d10d1dcb95f11b65611473c9840e13683cb5ec.tar.gz dino-26d10d1dcb95f11b65611473c9840e13683cb5ec.zip |
Add multiparty call support to libdino and xmpp-vala
Diffstat (limited to 'libdino/src/service/call_state.vala')
-rw-r--r-- | libdino/src/service/call_state.vala | 302 |
1 files changed, 302 insertions, 0 deletions
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 |