From 4ef50db3e581016365087759d5af8649e37ab8a7 Mon Sep 17 00:00:00 2001 From: fiaxh Date: Wed, 2 Feb 2022 21:37:05 +0100 Subject: Various call UI/UX improvements --- libdino/src/plugin/interfaces.vala | 2 +- libdino/src/service/call_peer_state.vala | 11 ++-- libdino/src/service/call_state.vala | 79 +++++++++++++++++++++++----- libdino/src/service/calls.vala | 45 +++++----------- libdino/src/service/notification_events.vala | 1 + 5 files changed, 87 insertions(+), 51 deletions(-) (limited to 'libdino') diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index fb80fef6..23e64373 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -96,7 +96,7 @@ public abstract interface ConversationAdditionPopulator : ConversationItemPopula public abstract interface VideoCallPlugin : Object { - public abstract bool supports(string media); + public abstract bool supports(string? media); // Video widget public abstract VideoCallWidget? create_widget(WidgetType type); diff --git a/libdino/src/service/call_peer_state.vala b/libdino/src/service/call_peer_state.vala index 09440371..902a0792 100644 --- a/libdino/src/service/call_peer_state.vala +++ b/libdino/src/service/call_peer_state.vala @@ -3,6 +3,7 @@ using Gee; using Xmpp; public class Dino.PeerState : Object { + public signal void stream_created(string media); public signal void counterpart_sends_video_updated(bool mute); public signal void info_received(Xep.JingleRtp.CallSessionInfo session_info); @@ -214,14 +215,14 @@ public class Dino.PeerState : Object { // 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) { + public Xep.JingleRtp.Stream? get_video_stream() { if (video_content_parameter != null) { return video_content_parameter.stream; } return null; } - public Xep.JingleRtp.Stream? get_audio_stream(Call call) { + public Xep.JingleRtp.Stream? get_audio_stream() { if (audio_content_parameter != null) { return audio_content_parameter.stream; } @@ -235,8 +236,8 @@ public class Dino.PeerState : Object { 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) + session.additional_content_add_incoming.connect((stream, content) => + on_incoming_content_add(stream, content.session, content) ); foreach (Xep.Jingle.Content content in session.contents) { @@ -358,6 +359,8 @@ public class Dino.PeerState : Object { } else if (media == "audio" && !we_should_send_audio) { mute_own_audio(true); } + + stream_created(media); } private void on_counterpart_mute_update(bool mute, string? media) { diff --git a/libdino/src/service/call_state.vala b/libdino/src/service/call_state.vala index 7d205f7f..03ee9595 100644 --- a/libdino/src/service/call_state.vala +++ b/libdino/src/service/call_state.vala @@ -9,6 +9,7 @@ public class Dino.CallState : Object { public signal void peer_left(Jid jid, PeerState peer_state, string? reason_name, string? reason_text); public StreamInteractor stream_interactor; + public Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin; public Call call; public Xep.Muji.GroupCall? group_call { get; set; } public Jid? parent_muc { get; set; } @@ -109,22 +110,25 @@ public class Dino.CallState : Object { terminated(call.account.bare_jid, null, null); } - public void end() { + public void end(string? reason_text = null) { var peers_cpy = new ArrayList(); peers_cpy.add_all(peers.values); if (group_call != null) { - stream_interactor.get_module(MucManager.IDENTITY).part(call.account, group_call.muc_jid); + XmppStream stream = stream_interactor.get_stream(call.account); + if (stream != null) { + stream.get_module(Xep.Muc.Module.IDENTITY).exit(stream, 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); + peer.end(Xep.Jingle.ReasonElement.SUCCESS, reason_text); } call.state = Call.State.ENDED; } else if (call.state == Call.State.RINGING) { foreach (PeerState peer in peers_cpy) { - peer.end(Xep.Jingle.ReasonElement.CANCEL); + peer.end(Xep.Jingle.ReasonElement.CANCEL, reason_text); } if (parent_muc != null && group_call != null) { XmppStream stream = stream_interactor.get_stream(call.account); @@ -138,7 +142,7 @@ public class Dino.CallState : Object { call.end_time = new DateTime.now_utc(); - terminated(call.account.bare_jid, null, null); + terminated(call.account.bare_jid, null, reason_text); } public void mute_own_audio(bool mute) { @@ -168,7 +172,7 @@ public class Dino.CallState : Object { 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); + stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, invitee, group_call.muc_jid, we_should_send_video, message_type); // If the peer hasn't accepted within a minute, retract the invite Timeout.add_seconds(60, () => { @@ -183,13 +187,43 @@ public class Dino.CallState : Object { 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.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, invitee, group_call.muc_jid, message_type); stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none"); } return false; }); } + public Plugins.MediaDevice? get_microphone_device() { + if (peers.is_empty) return null; + var audio_stream = peers.values.to_array()[0].get_audio_stream(); + return call_plugin.get_device(audio_stream, false); + } + + public Plugins.MediaDevice? get_speaker_device() { + if (peers.is_empty) return null; + var audio_stream = peers.values.to_array()[0].get_audio_stream(); + return call_plugin.get_device(audio_stream, true); + } + + public Plugins.MediaDevice? get_video_device() { + if (peers.is_empty) return null; + var video_stream = peers.values.to_array()[0].get_video_stream(); + return call_plugin.get_device(video_stream, false); + } + + public void set_audio_device(Plugins.MediaDevice? device) { + foreach (PeerState peer_state in peers.values) { + call_plugin.set_device(peer_state.get_audio_stream(), device); + } + } + + public void set_video_device(Plugins.MediaDevice? device) { + foreach (PeerState peer_state in peers.values) { + call_plugin.set_device(peer_state.get_video_stream(), device); + } + } + internal void rename_peer(Jid from_jid, Jid to_jid) { debug("[%s] Renaming %s to %s exists %s", call.account.bare_jid.to_string(), from_jid.to_string(), to_jid.to_string(), peers.has_key(from_jid).to_string()); PeerState? peer_state = peers[from_jid]; @@ -226,23 +260,35 @@ public class Dino.CallState : Object { 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) => { + debug("[%s] Peer left %s: %s %s (%i peers remaining)", call.account.bare_jid.to_string(), reason_text ?? "", reason_name ?? "", peer_state.jid.to_string(), peers.size); 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); + if (group_call != null) { + group_call.leave(stream_interactor.get_stream(call.account)); + on_call_terminated(peer_state.jid, we_terminated, null, "All participants have left the group call"); + } else { + 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 bool can_convert_into_groupcall() { + if (peers.size == 0) return false; + Jid peer = peers.keys.to_array()[0]; + bool peer_has_feature = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(call.account, peer, Xep.Muji.NS_URI); + bool can_initiate = stream_interactor.get_module(Calls.IDENTITY).can_initiate_groupcall(call.account); + return peer_has_feature && can_initiate; + } + public async void convert_into_group_call() { XmppStream stream = stream_interactor.get_stream(call.account); if (stream == null) return; - Jid? muc_jid = null; + Jid? muc_jid = stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[call.account]; if (muc_jid == null) { warning("Failed to initiate group call: MUC server not known."); return; @@ -320,11 +366,18 @@ public class Dino.CallState : Object { this.group_call.peer_left.connect((jid) => { debug("[%s] Group call peer left: %s", call.account.bare_jid.to_string(), jid.to_string()); + PeerState? peer_state = peers[jid]; 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"); + peer_left(jid, peer_state, Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC"); + peer_state.end(Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC"); + peers.unset(jid); }); + if (group_call.peers_to_connect_to.size > 3) { + end("Call too full - P2p calls don't work well with many participants"); + return; + } + // 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) diff --git a/libdino/src/service/calls.vala b/libdino/src/service/calls.vala index 741aa673..44790014 100644 --- a/libdino/src/service/calls.vala +++ b/libdino/src/service/calls.vala @@ -67,38 +67,24 @@ namespace Dino { return call_state; } - public async bool can_do_audio_calls_async(Conversation conversation) { - if (!can_do_audio_calls()) return false; - - 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() { + public bool can_we_do_calls(Account account) { Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin; if (plugin == null) return false; - return plugin.supports("audio"); + return plugin.supports(null); } - public async bool can_do_video_calls_async(Conversation conversation) { - if (!can_do_video_calls()) return false; - + public async bool can_conversation_do_calls(Conversation conversation) { 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); + bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart); + return is_private && can_initiate_groupcall(conversation.account); } } - private bool can_do_video_calls() { - Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin; - if (plugin == null) return false; - - return plugin.supports("video"); + public bool can_initiate_groupcall(Account account) { + return stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[account] != null; } public async Gee.List get_call_resources(Account account, Jid counterpart) { @@ -107,7 +93,10 @@ namespace Dino { XmppStream? stream = stream_interactor.get_stream(account); if (stream == null) return ret; - Gee.List? full_jids = stream.get_flag(Presence.Flag.IDENTITY).get_resources(counterpart); + Presence.Flag? presence_flag = stream.get_flag(Presence.Flag.IDENTITY); + if (presence_flag == null) return ret; + + Gee.List? full_jids = presence_flag.get_resources(counterpart); if (full_jids == null) return ret; foreach (Jid full_jid in full_jids) { @@ -148,11 +137,6 @@ namespace Dino { } private void on_incoming_call(Account account, Xep.Jingle.Session session) { - if (!can_do_audio_calls()) { - warning("Incoming call but no call support detected. Ignoring."); - return; - } - Jid? muji_muc = null; bool counterpart_wants_video = false; foreach (Xep.Jingle.Content content in session.contents) { @@ -268,7 +252,7 @@ namespace Dino { 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_states[call].parent_muc != null && 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. @@ -337,11 +321,6 @@ namespace Dino { 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()) { - warning("Incoming call but no call support detected. Ignoring."); - return; - } - 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; diff --git a/libdino/src/service/notification_events.vala b/libdino/src/service/notification_events.vala index 2408aadc..5b8db842 100644 --- a/libdino/src/service/notification_events.vala +++ b/libdino/src/service/notification_events.vala @@ -118,6 +118,7 @@ public class NotificationEvents : StreamInteractionModule, Object { } private async void on_call_incoming(Call call, CallState call_state, Conversation conversation, bool video) { + if (!stream_interactor.get_module(Calls.IDENTITY).can_we_do_calls(call.account)) return; string conversation_display_name = get_conversation_display_name(stream_interactor, conversation, null); NotificationProvider notifier = yield notifier.wait_async(); -- cgit v1.2.3-70-g09d2