From 3dc4d7f1558bb574eb99dade9a05727604e2e2ca Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 4 Nov 2021 17:35:11 +0100 Subject: Add (disabled) multi-party call UI --- main/CMakeLists.txt | 3 + main/data/call_widget.ui | 15 +- main/data/icons/dino-account-plus-symbolic.svg | 1 + main/data/theme.css | 50 +++- main/src/ui/application.vala | 14 +- main/src/ui/avatar_image.vala | 7 +- main/src/ui/call_window/call_bottom_bar.vala | 11 +- .../call_connection_details_window.vala | 84 ++++++ .../src/ui/call_window/call_encryption_button.vala | 20 +- main/src/ui/call_window/call_window.vala | 262 ++++++++-------- .../src/ui/call_window/call_window_controller.vala | 332 ++++++++++++++------- main/src/ui/call_window/participant_widget.vala | 129 ++++++++ .../contact_details/muc_config_form_provider.vala | 2 +- .../ui/conversation_content_view/call_widget.vala | 74 ++++- .../conversation_item_skeleton.vala | 1 - main/src/ui/conversation_titlebar/call_entry.vala | 32 +- main/src/ui/notifier_freedesktop.vala | 3 +- main/src/ui/notifier_gnotifications.vala | 2 +- 18 files changed, 745 insertions(+), 297 deletions(-) create mode 100644 main/data/icons/dino-account-plus-symbolic.svg create mode 100644 main/src/ui/call_window/call_connection_details_window.vala create mode 100644 main/src/ui/call_window/participant_widget.vala (limited to 'main') diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 4891abb0..3abe970a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -16,6 +16,7 @@ find_packages(MAIN_PACKAGES REQUIRED ) set(RESOURCE_LIST + icons/dino-account-plus-symbolic.svg icons/dino-changes-allowed-symbolic.svg icons/dino-changes-prevent-symbolic.svg icons/dino-conversation-list-placeholder-arrow.svg @@ -139,9 +140,11 @@ SOURCES src/ui/call_window/audio_settings_popover.vala src/ui/call_window/call_bottom_bar.vala + src/ui/call_window/call_connection_details_window.vala src/ui/call_window/call_encryption_button.vala src/ui/call_window/call_window.vala src/ui/call_window/call_window_controller.vala + src/ui/call_window/participant_widget.vala src/ui/call_window/video_settings_popover.vala src/ui/chat_input/chat_input_controller.vala diff --git a/main/data/call_widget.ui b/main/data/call_widget.ui index 47fb0046..8e2ee36c 100644 --- a/main/data/call_widget.ui +++ b/main/data/call_widget.ui @@ -66,19 +66,15 @@ True True - + True - - + end horizontal 5 10 True - True Reject @@ -99,6 +95,13 @@ + + + 10 + 5 + True + + diff --git a/main/data/icons/dino-account-plus-symbolic.svg b/main/data/icons/dino-account-plus-symbolic.svg new file mode 100644 index 00000000..cf743afa --- /dev/null +++ b/main/data/icons/dino-account-plus-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/main/data/theme.css b/main/data/theme.css index cf57ae96..36b3cb5b 100644 --- a/main/data/theme.css +++ b/main/data/theme.css @@ -114,6 +114,11 @@ window.dino-main .incoming-call-box { background: alpha(@theme_selected_bg_color, 0.1); } +window.dino-main .multiparty-participants { + border-top: 1px solid alpha(@theme_fg_color, 0.05); + background: alpha(@theme_fg_color, 0.04) +} + window.dino-main .dino-sidebar > frame.collapsed { border-bottom: 1px solid @borders; } @@ -280,20 +285,27 @@ box.dino-input-error label.input-status-highlight-once { margin: 0; } -.dino-call-window .encryption-box { +.dino-call-window .black-element { color: rgba(255,255,255,0.7); border-radius: 5px; background: rgba(0,0,0,0.5); - padding: 0px; border: none; box-shadow: none; } -.dino-call-window .encryption-box.unencrypted { +.dino-call-window label.black-element { + padding: 5px; +} + +.dino-call-window button.black-element { + padding: 0; +} + +.dino-call-window button.black-element.unencrypted { color: @error_color; } -.dino-call-window .encryption-box:hover { +.dino-call-window .black-element:hover { background: rgba(20,20,20,0.5); } @@ -303,22 +315,46 @@ box.dino-input-error label.input-status-highlight-once { border-radius: 0; } -.dino-call-window .call-header-bar, -.dino-call-window .call-header-bar image { +.dino-call-window .call-header-bar { color: #ededec; } +.dino-call-window .call-header-bar button image { + color: alpha(white, 0.7); +} + +.dino-call-window .call-header-bar button:hover image { + color: white; +} + +.dino-call-window .participant-title-button { + background: none; + border: 0; + border-radius: 0; + box-shadow: none; +} + .dino-call-window .call-bottom-bar { background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3)); border: 0; } -.dino-call-window .video-placeholder-box { +.dino-call-window { background-color: #212121; } +.dino-call-window .participant-name { + color: white; + text-shadow: black; +} + .dino-call-window .text-no-controls { + color: black; background: white; border-radius: 5px; padding: 5px 10px; +} + +.dino-call-window .own-video { + box-shadow: 0 0 2px 0 rgba(0,0,0,0.5); } \ No newline at end of file diff --git a/main/src/ui/application.vala b/main/src/ui/application.vala index bed6d01b..9f48caec 100644 --- a/main/src/ui/application.vala +++ b/main/src/ui/application.vala @@ -209,18 +209,24 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application { SimpleAction accept_call_action = new SimpleAction("accept-call", VariantType.INT32); accept_call_action.activate.connect((variant) => { Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32()); - stream_interactor.get_module(Calls.IDENTITY).accept_call(call); + CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call]; + if (call_state == null) return; + + call_state.accept(); var call_window = new CallWindow(); - call_window.controller = new CallWindowController(call_window, call, stream_interactor); + call_window.controller = new CallWindowController(call_window, call_state, stream_interactor); call_window.present(); }); add_action(accept_call_action); - SimpleAction deny_call_action = new SimpleAction("deny-call", VariantType.INT32); + SimpleAction deny_call_action = new SimpleAction("reject-call", VariantType.INT32); deny_call_action.activate.connect((variant) => { Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32()); - stream_interactor.get_module(Calls.IDENTITY).reject_call(call); + CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call]; + if (call_state == null) return; + + call_state.reject(); }); add_action(deny_call_action); } diff --git a/main/src/ui/avatar_image.vala b/main/src/ui/avatar_image.vala index 81aa3ce1..f7731373 100644 --- a/main/src/ui/avatar_image.vala +++ b/main/src/ui/avatar_image.vala @@ -9,6 +9,7 @@ public class AvatarImage : Misc { public int height { get; set; default = 35; } public int width { get; set; default = 35; } public bool allow_gray { get; set; default = true; } + public bool force_gray { get; set; default = false; } public StreamInteractor? stream_interactor { get; set; } public AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } public MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } @@ -78,14 +79,14 @@ public class AvatarImage : Misc { string user_color = Util.get_avatar_hex_color(stream_interactor, account, conversation.counterpart, conversation); if (avatar_manager.has_avatar_cached(account, conversation.counterpart)) { drawer = new AvatarDrawer().tile(avatar_manager.get_cached_avatar(account, conversation.counterpart), "#", user_color); - if (allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); + if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); } else { Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, account); if (muc_manager.is_private_room(account, conversation.counterpart) && occupants != null && occupants.size > 0) { jids = occupants.to_array(); } else { drawer = new AvatarDrawer().tile(null, "#", user_color); - if (allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); + if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); } try_load_avatar_async(conversation.counterpart); } @@ -116,7 +117,7 @@ public class AvatarImage : Misc { if (jids.length > 4) { drawer.plus(); } - if (allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); + if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); } diff --git a/main/src/ui/call_window/call_bottom_bar.vala b/main/src/ui/call_window/call_bottom_bar.vala index 8a0604b3..b3fa2093 100644 --- a/main/src/ui/call_window/call_bottom_bar.vala +++ b/main/src/ui/call_window/call_bottom_bar.vala @@ -25,17 +25,12 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END }; public VideoSettingsPopover? video_settings_popover; - public CallEntryptionButton encryption_button = new CallEntryptionButton() { relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END }; - private Label label = new Label("") { margin=20, halign=Align.CENTER, valign=Align.CENTER, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, visible=true }; private Stack stack = new Stack() { visible=true }; public CallBottomBar() { Object(orientation:Orientation.HORIZONTAL, spacing:0); - Overlay default_control = new Overlay() { visible=true }; - default_control.add_overlay(encryption_button); - Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true }; audio_button.add(audio_image); @@ -66,11 +61,9 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { button_hang.clicked.connect(() => hang_up()); main_buttons.add(button_hang); - default_control.add(main_buttons); - label.get_style_context().add_class("text-no-controls"); - stack.add_named(default_control, "control-buttons"); + stack.add_named(main_buttons, "control-buttons"); stack.add_named(label, "label"); this.add(stack); @@ -159,6 +152,6 @@ public class Dino.Ui.CallBottomBar : Gtk.Box { } public bool is_menu_active() { - return video_settings_button.active || audio_settings_button.active || encryption_button.active; + return video_settings_button.active || audio_settings_button.active; } } \ 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 new file mode 100644 index 00000000..b292b971 --- /dev/null +++ b/main/src/ui/call_window/call_connection_details_window.vala @@ -0,0 +1,84 @@ +using Gtk; + +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("?") { xalign=0, visible=true }; + public Label audio_recv_bps = new Label("?") { xalign=0, visible=true }; + public Label audio_codec = new Label("?") { 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_codec = new Label("") { xalign=0, visible=true }; + + private int row_at = 0; + private bool video_added = false; + private PeerInfo? prev_peer_info = null; + + public CallConnectionDetailsWindow() { + grid.attach(new Label("Audio") { 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); + + this.child = grid; + } + + private void put_row(string label) { + grid.attach(new Label(label) { xalign=0, visible=true }, 0, row_at, 1, 1); + } + + 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(); + + 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; + + 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(); + } + prev_peer_info = peer_info; + } + + private void add_video_widgets() { + if (video_added) return; + + grid.attach(new Label("Video") { 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); + + video_added = true; + } + } +} + diff --git a/main/src/ui/call_window/call_encryption_button.vala b/main/src/ui/call_window/call_encryption_button.vala index 1d785d51..a7081954 100644 --- a/main/src/ui/call_window/call_encryption_button.vala +++ b/main/src/ui/call_window/call_encryption_button.vala @@ -2,19 +2,21 @@ using Dino.Entities; using Gtk; using Pango; -public class Dino.Ui.CallEntryptionButton : MenuButton { +public class Dino.Ui.CallEncryptionButton : MenuButton { - private Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { visible=true }; + private Image encryption_image = new Image.from_icon_name("", IconSize.BUTTON) { visible=true }; + private bool has_been_set = false; + public bool controls_active { get; set; default=false; } - construct { + public CallEncryptionButton() { + this.opacity = 0; add(encryption_image); - get_style_context().add_class("encryption-box"); this.set_popover(popover); + + this.notify["controls-active"].connect(update_opacity); } public void set_icon(bool encrypted, string? icon_name) { - this.visible = true; - if (encrypted) { encryption_image.set_from_icon_name(icon_name ?? "changes-prevent-symbolic", IconSize.BUTTON); get_style_context().remove_class("unencrypted"); @@ -22,6 +24,8 @@ public class Dino.Ui.CallEntryptionButton : MenuButton { encryption_image.set_from_icon_name(icon_name ?? "changes-allow-symbolic", IconSize.BUTTON); get_style_context().add_class("unencrypted"); } + has_been_set = true; + update_opacity(); } public void set_info(string? title, bool show_keys, Xmpp.Xep.Jingle.ContentEncryption? audio_encryption, Xmpp.Xep.Jingle.ContentEncryption? video_encryption) { @@ -51,6 +55,10 @@ public class Dino.Ui.CallEntryptionButton : MenuButton { popover.add(box); } + public void update_opacity() { + this.opacity = controls_active && has_been_set ? 1 : 0; + } + private Grid create_media_encryption_grid(Xmpp.Xep.Jingle.ContentEncryption? encryption) { Grid ret = new Grid() { row_spacing=3, column_spacing=5, visible=true }; if (encryption.peer_key.length > 0) { diff --git a/main/src/ui/call_window/call_window.vala b/main/src/ui/call_window/call_window.vala index 3b3d4dc2..fab424bf 100644 --- a/main/src/ui/call_window/call_window.vala +++ b/main/src/ui/call_window/call_window.vala @@ -1,87 +1,155 @@ +using Gee; +using Xmpp; using Dino.Entities; using Gtk; namespace Dino.Ui { public class CallWindow : Gtk.Window { - public string counterpart_display_name { get; set; } - // TODO should find another place for this + public signal void menu_dump_dot(); + public CallWindowController controller; public Overlay overlay = new Overlay() { visible=true }; + public Grid grid = new Grid() { visible=true }; public EventBox event_box = new EventBox() { visible=true }; public CallBottomBar bottom_bar = new CallBottomBar() { visible=true }; public Revealer bottom_bar_revealer = new Revealer() { valign=Align.END, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true }; - public HeaderBar header_bar = new HeaderBar() { show_close_button=true, visible=true }; - public Revealer header_bar_revealer = new Revealer() { valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true }; - public Stack stack = new Stack() { visible=true }; - public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { expand=true, visible=true }; + public HeaderBar header_bar = new HeaderBar() { valign=Align.START, halign=Align.END, show_close_button=true, visible=true }; + 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 }; private Widget? own_video = null; - private Box? own_video_border = new Box(Orientation.HORIZONTAL, 0) { expand=true }; // hack to draw a border around our own video, since we apparently can't draw a border around the Gst widget + private HashMap participant_widgets = new HashMap(); + private ArrayList participants = new ArrayList(); private int own_video_width = 150; private int own_video_height = 100; - private bool hide_controll_elements = false; - private uint hide_controll_handler = 0; - private Widget? main_widget = null; + private bool hide_control_elements = false; + private uint hide_control_handler = 0; + public bool controls_active { get; set; default=true; } construct { + Util.force_css(header_bar, "* { background: none; border: 0; border-radius: 0; }"); header_bar.get_style_context().add_class("call-header-bar"); header_bar_revealer.add(header_bar); + bottom_bar_revealer.add(bottom_bar); + invite_button.get_style_context().add_class("black-element"); + invite_button_revealer.add(invite_button); + own_video_box.get_style_context().add_class("own-video"); this.get_style_context().add_class("dino-call-window"); - bottom_bar_revealer.add(bottom_bar); - + overlay.add(grid); overlay.add_overlay(own_video_box); - overlay.add_overlay(own_video_border); overlay.add_overlay(bottom_bar_revealer); overlay.add_overlay(header_bar_revealer); + overlay.add_overlay(invite_button_revealer); + overlay.get_child_position.connect(on_get_child_position); event_box.add(overlay); add(event_box); - - Util.force_css(own_video_border, "* { border: 1px solid #616161; background-color: transparent; }"); } public CallWindow() { - event_box.events |= Gdk.EventMask.POINTER_MOTION_MASK; - event_box.events |= Gdk.EventMask.ENTER_NOTIFY_MASK; - event_box.events |= Gdk.EventMask.LEAVE_NOTIFY_MASK; - - this.bind_property("counterpart-display-name", header_bar, "title", BindingFlags.SYNC_CREATE); - this.bind_property("counterpart-display-name", bottom_bar, "counterpart-display-name", BindingFlags.SYNC_CREATE); + this.bind_property("controls-active", bottom_bar_revealer, "reveal-child", BindingFlags.SYNC_CREATE); + this.bind_property("controls-active", header_bar_revealer, "reveal-child", BindingFlags.SYNC_CREATE); + this.bind_property("controls-active", invite_button_revealer, "reveal-child", BindingFlags.SYNC_CREATE); event_box.motion_notify_event.connect(reveal_control_elements); event_box.enter_notify_event.connect(reveal_control_elements); event_box.leave_notify_event.connect(reveal_control_elements); this.configure_event.connect(reveal_control_elements); // upon resizing - this.configure_event.connect(update_own_video_position); + + this.configure_event.connect(reposition_participant_widgets); this.set_titlebar(new OutsideHeaderBar(this.header_bar) { visible=true }); reveal_control_elements(); } - public void set_video_fallback(StreamInteractor stream_interactor, Conversation conversation) { - hide_controll_elements = false; + public void add_participant(string participant, ParticipantWidget participant_widget) { + participant_widget.visible = true; + this.bind_property("controls-active", participant_widget, "controls-active", BindingFlags.SYNC_CREATE); + this.bind_property("controls-active", participant_widget.encryption_button, "controls-active", BindingFlags.SYNC_CREATE); + + participants.add(participant); + participant_widgets[participant] = participant_widget; + grid.attach(participant_widget, 0, 0); + + reposition_participant_widgets(); + } + + public void remove_participant(string participant) { + participants.remove(participant); + grid.remove(participant_widgets[participant]); + participant_widgets.unset(participant); - Box box = new Box(Orientation.HORIZONTAL, 0) { visible=true }; - box.get_style_context().add_class("video-placeholder-box"); - AvatarImage avatar = new AvatarImage() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100, visible=true }; - avatar.set_conversation(stream_interactor, conversation); - box.add(avatar); + reposition_participant_widgets(); + } - set_new_main_widget(box); + public void set_video(string participant, Widget widget) { + participant_widgets[participant].set_video(widget); + hide_control_elements = true; + timeout_hide_control_elements(); } - public void set_video(Widget widget) { - hide_controll_elements = true; + public void set_placeholder(string participant, Conversation? conversation, StreamInteractor stream_interactor) { + participant_widgets[participant].set_placeholder(conversation, stream_interactor); + hide_control_elements = false; + foreach (ParticipantWidget participant_widget in participant_widgets.values) { + if (participant_widget.shows_video) { + hide_control_elements = true; + } + } - widget.visible = true; - set_new_main_widget(widget); + if (!hide_control_elements) { + reveal_control_elements(); + } + } + + private bool reposition_participant_widgets() { + int width, height; + this.get_size(out width,out height); + reposition_participant_widgets_rec(participants, width, height, 0, 0, 0, 0); + return false; + } + + private void reposition_participant_widgets_rec(ArrayList participants, int width, int height, int margin_top, int margin_right, int margin_bottom, int margin_left) { + if (participants.size == 0) return; + + if (participants.size == 1) { + participant_widgets[participants[0]].margin_top = margin_top; + participant_widgets[participants[0]].margin_end = margin_right; + participant_widgets[participants[0]].margin_bottom = margin_bottom; + participant_widgets[participants[0]].margin_start = margin_left; + + participant_widgets[participants[0]].on_lowest_row_changed(margin_bottom == 0); + participant_widgets[participants[0]].on_highest_row_changed(margin_top == 0); + return; + } + + ArrayList first_part = new ArrayList(); + ArrayList last_part = new ArrayList(); + + for (int i = 0; i < participants.size; i++) { + if (i < Math.ceil((double)participants.size / (double)2)) { + first_part.add(participants[i]); + } else { + last_part.add(participants[i]); + } + } + + if (width > height) { + reposition_participant_widgets_rec(first_part, width / 2, height, margin_top, margin_right + width / 2, margin_bottom, margin_left); + reposition_participant_widgets_rec(last_part, width / 2, height, margin_top, margin_right, margin_bottom, margin_left + width / 2); + } else { + reposition_participant_widgets_rec(first_part, width, height / 2, margin_top, margin_right, margin_bottom + height / 2, margin_left); + reposition_participant_widgets_rec(last_part, width, height / 2, margin_top + height / 2, margin_right, margin_bottom, margin_left); + } } public void set_own_video(Widget? widget_) { @@ -92,13 +160,7 @@ namespace Dino.Ui { own_video = new Box(Orientation.HORIZONTAL, 0) { expand=true }; } own_video.visible = true; - own_video.width_request = 150; - own_video.height_request = 100; own_video_box.add(own_video); - - own_video_border.visible = true; - - update_own_video_position(); } public void set_own_video_ratio(int width, int height) { @@ -109,78 +171,25 @@ namespace Dino.Ui { this.own_video_width = width * 100 / height; this.own_video_height = 100; } - - own_video.width_request = own_video_width; - own_video.height_request = own_video_height; - - update_own_video_position(); } public void unset_own_video() { own_video_box.foreach((widget) => { own_video_box.remove(widget); }); - - own_video_border.visible = false; - } - - public void set_test_video() { - hide_controll_elements = true; - - var pipeline = new Gst.Pipeline(null); - var src = Gst.ElementFactory.make("videotestsrc", null); - pipeline.add(src); - Gst.Video.Sink sink = (Gst.Video.Sink) Gst.ElementFactory.make("gtksink", null); - Gtk.Widget widget; - sink.get("widget", out widget); - widget.unparent(); - pipeline.add(sink); - src.link(sink); - widget.visible = true; - - pipeline.set_state(Gst.State.PLAYING); - - sink.get_static_pad("sink").notify["caps"].connect(() => { - int width, height; - sink.get_static_pad("sink").caps.get_structure(0).get_int("width", out width); - sink.get_static_pad("sink").caps.get_structure(0).get_int("height", out height); - widget.width_request = width; - widget.height_request = height; - }); - - set_new_main_widget(widget); } - private void set_new_main_widget(Widget widget) { - if (main_widget != null) overlay.remove(main_widget); - overlay.add(widget); - main_widget = widget; + public void set_status(string participant_id, string state) { + participant_widgets[participant_id].set_status(state); } - public void set_status(string state) { - switch (state) { - case "requested": - header_bar.subtitle = _("Calling…"); - break; - case "ringing": - header_bar.subtitle = _("Ringing…"); - break; - case "establishing": - header_bar.subtitle = _("Connecting…"); - break; - default: - header_bar.subtitle = null; - break; - } - } - - public void show_counterpart_ended(string? reason_name, string? reason_text) { - hide_controll_elements = false; + public void show_counterpart_ended(string who_terminated, string? reason_name, string? reason_text) { + hide_control_elements = false; reveal_control_elements(); string text = ""; if (reason_name == Xmpp.Xep.Jingle.ReasonElement.SUCCESS) { - text = _("%s ended the call").printf(counterpart_display_name); + text = _("%s ended the call").printf(who_terminated); } else if (reason_name == Xmpp.Xep.Jingle.ReasonElement.DECLINE || reason_name == Xmpp.Xep.Jingle.ReasonElement.BUSY) { - text = _("%s declined the call").printf(counterpart_display_name); + text = _("%s declined the call").printf(who_terminated); } else { text = "The call has been terminated: " + (reason_name ?? "") + " " + (reason_text ?? ""); } @@ -188,48 +197,53 @@ namespace Dino.Ui { bottom_bar.show_counterpart_ended(text); } - public bool reveal_control_elements() { + private bool reveal_control_elements() { if (!bottom_bar_revealer.child_revealed) { - bottom_bar_revealer.set_reveal_child(true); - header_bar_revealer.set_reveal_child(true); + controls_active = true; } - if (hide_controll_handler != 0) { - Source.remove(hide_controll_handler); - hide_controll_handler = 0; + timeout_hide_control_elements(); + return false; + } + + private void timeout_hide_control_elements() { + if (hide_control_handler != 0) { + Source.remove(hide_control_handler); + hide_control_handler = 0; } - if (!hide_controll_elements) { - return false; + if (!hide_control_elements) { + return; } - hide_controll_handler = Timeout.add_seconds(3, () => { - if (!hide_controll_elements) { + hide_control_handler = Timeout.add_seconds(3, () => { + if (!hide_control_elements) { return false; } if (bottom_bar.is_menu_active()) { - return true; + return false; } - header_bar_revealer.set_reveal_child(false); - bottom_bar_revealer.set_reveal_child(false); - hide_controll_handler = 0; + controls_active = false; + + hide_control_handler = 0; return false; }); - return false; } - private bool update_own_video_position() { - if (own_video == null) return false; - - int width, height; - this.get_size(out width,out height); - - own_video.margin_end = own_video.margin_bottom = own_video_border.margin_end = own_video_border.margin_bottom = 20; - own_video.margin_start = own_video_border.margin_start = width - own_video_width - 20; - own_video.margin_top = own_video_border.margin_top = height - own_video_height - 20; - + private bool on_get_child_position(Widget widget, out Gdk.Rectangle allocation) { + if (widget == own_video_box) { + int width, height; + this.get_size(out width,out height); + + allocation = Gdk.Rectangle(); + allocation.width = own_video_width; + allocation.height = own_video_height; + allocation.x = width - own_video_width - 20; + allocation.y = height - own_video_height - 20; + return true; + } return false; } } diff --git a/main/src/ui/call_window/call_window_controller.vala b/main/src/ui/call_window/call_window_controller.vala index b07b41b1..dbf2106c 100644 --- a/main/src/ui/call_window/call_window_controller.vala +++ b/main/src/ui/call_window/call_window_controller.vala @@ -1,3 +1,5 @@ +using Xmpp; +using Gee; using Dino.Entities; using Gtk; @@ -5,103 +7,227 @@ public class Dino.Ui.CallWindowController : Object { private CallWindow call_window; private Call call; - private Conversation conversation; + private CallState call_state; private StreamInteractor stream_interactor; private Calls calls; private Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin; private Plugins.VideoCallWidget? own_video = null; - private Plugins.VideoCallWidget? counterpart_video = null; + private HashMap participant_videos = new HashMap(); + private HashMap participant_widgets = new HashMap(); + private HashMap peer_states = new HashMap(); private int window_height = -1; private int window_width = -1; private bool window_size_changed = false; + private ulong[] call_window_handler_ids = new ulong[0]; + private ulong[] bottom_bar_handler_ids = new ulong[0]; + private ulong[] invite_handler_ids = new ulong[0]; - public CallWindowController(CallWindow call_window, Call call, StreamInteractor stream_interactor) { + public CallWindowController(CallWindow call_window, CallState call_state, StreamInteractor stream_interactor) { this.call_window = call_window; - this.call = call; + this.call = call_state.call; + this.call_state = call_state; this.stream_interactor = stream_interactor; this.calls = stream_interactor.get_module(Calls.IDENTITY); - this.conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(call.counterpart.bare_jid, call.account, Conversation.Type.CHAT); this.own_video = call_plugin.create_widget(Plugins.WidgetType.GTK); - this.counterpart_video = call_plugin.create_widget(Plugins.WidgetType.GTK); - call_window.counterpart_display_name = Util.get_conversation_display_name(stream_interactor, conversation); call_window.set_default_size(704, 528); // 640x480 * 1.1 - call_window.set_video_fallback(stream_interactor, conversation); - this.call_window.bottom_bar.video_enabled = calls.should_we_send_video(call); + this.call_window.bottom_bar.video_enabled = call_state.should_we_send_video(); - if (call.direction == Call.DIRECTION_INCOMING) { - call_window.set_status("establishing"); - } else { - call_window.set_status("requested"); + call_state.terminated.connect((who_terminated, reason_name, reason_text) => { + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(who_terminated.bare_jid, call.account, Conversation.Type.CHAT); + string display_name = conversation != null ? Util.get_conversation_display_name(stream_interactor, conversation) : who_terminated.bare_jid.to_string(); + + call_window.show_counterpart_ended(display_name, reason_name, reason_text); + Timeout.add_seconds(3, () => { + call_window.close(); + call_window.destroy(); + + return false; + }); + }); + call_state.peer_joined.connect((jid, peer_state) => { + connect_peer_signals(peer_state); + add_new_participant(peer_state.internal_id, peer_state.jid); + }); + call_state.peer_left.connect((jid, peer_state, reason_name, reason_text) => { + remove_participant(peer_state.internal_id); + }); + + foreach (PeerState peer_state in call_state.peers.values) { + connect_peer_signals(peer_state); + add_new_participant(peer_state.internal_id, peer_state.jid); } - call_window.bottom_bar.hang_up.connect(() => { - calls.end_call(conversation, call); + // Call window signals + + bottom_bar_handler_ids += call_window.bottom_bar.hang_up.connect(() => { + call_state.end(); call_window.close(); call_window.destroy(); this.dispose(); }); - call_window.destroy.connect(() => { - calls.end_call(conversation, call); + call_window_handler_ids += call_window.destroy.connect(() => { + call_state.end(); this.dispose(); }); - - call_window.bottom_bar.notify["audio-enabled"].connect(() => { - calls.mute_own_audio(call, !call_window.bottom_bar.audio_enabled); + bottom_bar_handler_ids += call_window.bottom_bar.notify["audio-enabled"].connect(() => { + call_state.mute_own_audio(!call_window.bottom_bar.audio_enabled); }); - call_window.bottom_bar.notify["video-enabled"].connect(() => { - calls.mute_own_video(call, !call_window.bottom_bar.video_enabled); + bottom_bar_handler_ids += call_window.bottom_bar.notify["video-enabled"].connect(() => { + call_state.mute_own_video(!call_window.bottom_bar.video_enabled); update_own_video(); }); + call_window_handler_ids += call_window.configure_event.connect((event) => { + if (window_width == -1 || window_height == -1) return false; + int current_height = this.call_window.get_allocated_height(); + int current_width = this.call_window.get_allocated_width(); + if (window_width != current_width || window_height != current_height) { + debug("Call window size changed by user. Disabling auto window-to-video size adaptation. %i->%i x %i->%i", window_width, current_width, window_height, current_height); + window_size_changed = true; + } + return false; + }); + call_window_handler_ids += call_window.realize.connect(() => { + capture_window_size(); + }); + invite_handler_ids += call_window.invite_button.clicked.connect(() => { + Gee.List acc_list = new ArrayList(Account.equals_func); + acc_list.add(call.account); + SelectContactDialog add_chat_dialog = new SelectContactDialog(stream_interactor, acc_list); + add_chat_dialog.set_transient_for((Window) call_window.get_toplevel()); + add_chat_dialog.title = _("Invite to Call"); + add_chat_dialog.ok_button.label = _("Invite"); + add_chat_dialog.selected.connect((account, jid) => { + call_state.invite_to_call.begin(jid); + }); + add_chat_dialog.present(); + }); - calls.counterpart_sends_video_updated.connect((call, mute) => { + calls.conference_info_received.connect((call, conference_info) => { if (!this.call.equals(call)) return; + var participants = new ArrayList(); + participants.add_all(participant_videos.keys); + foreach (string participant in participants) { + remove_participant(participant); + } + foreach (Jid participant in conference_info.users.keys) { + add_new_participant(participant.to_string(), participant); + } + }); + + own_video.resolution_changed.connect((width, height) => { + if (width == 0 || height == 0) return; + call_window.set_own_video_ratio((int)width, (int)height); + }); + + call_window.menu_dump_dot.connect(() => { call_plugin.dump_dot(); }); + + update_own_video(); + } + + private void connect_peer_signals(PeerState peer_state) { + string peer_id = peer_state.internal_id; + Jid peer_jid = peer_state.jid; + peer_states[peer_id] = peer_state; + + peer_state.connection_ready.connect(() => { + call_window.set_status(peer_state.internal_id, ""); + if (participant_widgets.size == 1) { + // This is the first peer. + // If it can do MUJI, show invite button. + call_window.invite_button_revealer.visible = true; +// stream_interactor.get_module(EntityInfo.IDENTITY).has_feature.begin(call.account, peer_state.jid, Xep.Muji.NS_URI, (_, res) => { +// bool has_feature = stream_interactor.get_module(EntityInfo.IDENTITY).has_feature.end(res); +// call_window.invite_button_revealer.visible = has_feature; +// }); + + call_plugin.devices_changed.connect((media, incoming) => { + if (media == "audio") update_audio_device_choices(); + if (media == "video") update_video_device_choices(); + }); + + update_audio_device_choices(); + update_video_device_choices(); + } else if (participant_widgets.size >= 1) { + call_window.invite_button_revealer.visible = true; + } + }); + peer_state.counterpart_sends_video_updated.connect((mute) => { if (mute) { - call_window.set_video_fallback(stream_interactor, conversation); - counterpart_video.detach(); + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(peer_jid.bare_jid, call.account, Conversation.Type.CHAT); + call_window.set_placeholder(peer_id, conversation, stream_interactor); + participant_videos[peer_id].detach(); } else { - if (!(counterpart_video is Widget)) return; - Widget widget = (Widget) counterpart_video; - call_window.set_video(widget); - counterpart_video.display_stream(calls.get_video_stream(call)); + if (!(participant_videos[peer_id] is Widget)) return; + Widget widget = (Widget) participant_videos[peer_id]; + call_window.set_video(peer_id, widget); + participant_videos[peer_id].display_stream(peer_state.get_video_stream(call), peer_jid); } }); - calls.info_received.connect((call, session_info) => { - if (!this.call.equals(call)) return; + peer_state.info_received.connect((session_info) => { if (session_info == Xmpp.Xep.JingleRtp.CallSessionInfo.RINGING) { - call_window.set_status("ringing"); + call_window.set_status(peer_state.internal_id, "ringing"); } }); - calls.encryption_updated.connect((call, audio_encryption, video_encryption, same) => { - if (!this.call.equals(call)) return; + peer_state.encryption_updated.connect((audio_encryption, video_encryption, same) => { + update_encryption_indicator(participant_widgets[peer_id].encryption_button, audio_encryption, video_encryption, same); + }); + } - string? title = null; - string? icon_name = null; - bool show_keys = true; - Plugins.Registry registry = Dino.Application.get_default().plugin_registry; - Plugins.CallEncryptionEntry? encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null; - if (encryption_entry != null) { - Plugins.CallEncryptionWidget? encryption_widgets = encryption_entry.get_widget(call.account, audio_encryption); - if (encryption_widgets != null) { - title = encryption_widgets.get_title(); - icon_name = encryption_widgets.get_icon_name(); - show_keys = encryption_widgets.show_keys(); - } + private void update_encryption_indicator(CallEncryptionButton encryption_button, Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption, bool same) { + string? title = null; + string? icon_name = null; + bool show_keys = true; + Plugins.Registry registry = Dino.Application.get_default().plugin_registry; + Plugins.CallEncryptionEntry? encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null; + if (encryption_entry != null) { + Plugins.CallEncryptionWidget? encryption_widgets = encryption_entry.get_widget(call.account, audio_encryption); + if (encryption_widgets != null) { + title = encryption_widgets.get_title(); + icon_name = encryption_widgets.get_icon_name(); + show_keys = encryption_widgets.show_keys(); } - call_window.bottom_bar.encryption_button.set_info(title, show_keys, audio_encryption, same ? null :video_encryption); - call_window.bottom_bar.encryption_button.set_icon(audio_encryption != null, icon_name); - }); + } - own_video.resolution_changed.connect((width, height) => { - if (width == 0 || height == 0) return; - call_window.set_own_video_ratio((int)width, (int)height); + encryption_button.set_info(title, show_keys, audio_encryption, same ? null : video_encryption); + encryption_button.set_icon(audio_encryption != null, icon_name); + } + + private void add_new_participant(string participant_id, Jid jid) { + if (participant_widgets.has_key(participant_id)) { + warning("[%s] Attempted to add same participant twice: %s", call.account.bare_jid.to_string(), jid.to_string()); + return; + } + debug("[%s] Call window controller | Add participant: %s", call.account.bare_jid.to_string(), jid.to_string()); + + Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jid.bare_jid, call.account, Conversation.Type.CHAT); + string participant_name = conversation != null ? Util.get_conversation_display_name(stream_interactor, conversation) : jid.bare_jid.to_string(); + + ParticipantWidget participant_widget = new ParticipantWidget(participant_name); + participant_widget.menu_button.clicked.connect((event) => { + var conn_details_window = new CallConnectionDetailsWindow() { title=participant_name, visible=true }; + conn_details_window.update_content(peer_states[participant_id].get_info()); + uint timeout_handle_id = Timeout.add_seconds(1, () => { + conn_details_window.update_content(peer_states[participant_id].get_info()); + return true; + }); + conn_details_window.set_transient_for(call_window); + conn_details_window.destroy.connect(() => Source.remove(timeout_handle_id)); + conn_details_window.present(); + this.call_window.destroy.connect(() => conn_details_window.close() ); }); - counterpart_video.resolution_changed.connect((width, height) => { - if (window_size_changed) return; + participant_widgets[participant_id] = participant_widget; + + call_window.add_participant(participant_id, participant_widget); + + participant_videos[participant_id] = call_plugin.create_widget(Plugins.WidgetType.GTK); + + participant_videos[participant_id].resolution_changed.connect((width, height) => { + if (window_size_changed || participant_widgets.size > 1) return; if (width == 0 || height == 0) return; if (width > height) { call_window.resize(704, (int) (height * 704 / width)); @@ -110,24 +236,22 @@ public class Dino.Ui.CallWindowController : Object { } capture_window_size(); }); - call_window.configure_event.connect((event) => { - if (window_width == -1 || window_height == -1) return false; - int current_height = this.call_window.get_allocated_height(); - int current_width = this.call_window.get_allocated_width(); - if (window_width != current_width || window_height != current_height) { - debug("Call window size changed by user. Disabling auto window-to-video size adaptation. %i->%i x %i->%i", window_width, current_width, window_height, current_height); - window_size_changed = true; - } - return false; - }); - call_window.realize.connect(() => { - capture_window_size(); - }); - call.notify["state"].connect(on_call_state_changed); - calls.call_terminated.connect(on_call_terminated); + participant_widget.set_placeholder(conversation, stream_interactor); + if (call.direction == Call.DIRECTION_INCOMING) { + call_window.set_status(participant_id, "establishing"); + } else { + call_window.set_status(participant_id, "requested"); + } + } - update_own_video(); + private void remove_participant(string participant_id) { + if (peer_states.has_key(participant_id)) debug(@"[%s] Call window controller | Remove participant: %s", call.account.bare_jid.to_string(), peer_states[participant_id].jid.to_string()); + + participant_videos.unset(participant_id); + participant_widgets.unset(participant_id); + peer_states.unset(participant_id); + call_window.remove_participant(participant_id); } private void capture_window_size() { @@ -137,33 +261,6 @@ public class Dino.Ui.CallWindowController : Object { this.window_width = this.call_window.get_allocated_width(); } - private void on_call_state_changed() { - if (call.state == Call.State.IN_PROGRESS) { - call_window.set_status(""); - call_plugin.devices_changed.connect((media, incoming) => { - if (media == "audio") update_audio_device_choices(); - if (media == "video") update_video_device_choices(); - }); - - update_audio_device_choices(); - update_video_device_choices(); - } - } - - private void on_call_terminated(Call call, string? reason_name, string? reason_text) { - call_window.show_counterpart_ended(reason_name, reason_text); - Timeout.add_seconds(3, () => { - call.notify["state"].disconnect(on_call_state_changed); - calls.call_terminated.disconnect(on_call_terminated); - - - call_window.close(); - call_window.destroy(); - - return false; - }); - } - private void update_audio_device_choices() { if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) { call_window.bottom_bar.show_audio_device_error(); @@ -190,13 +287,13 @@ public class Dino.Ui.CallWindowController : Object { });*/ } - private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) { + /*private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) { Xmpp.Xep.JingleRtp.Stream stream = calls.get_audio_stream(call); if (stream != null) { audio_settings_popover.current_microphone_device = call_plugin.get_device(stream, false); audio_settings_popover.current_speaker_device = call_plugin.get_device(stream, true); } - } + }*/ private void update_video_device_choices() { int device_count = call_plugin.get_devices("video", false).size; @@ -223,12 +320,37 @@ public class Dino.Ui.CallWindowController : Object { });*/ } - private void update_current_video_device(VideoSettingsPopover video_settings_popover) { + public void add_test_video() { + var pipeline = new Gst.Pipeline(null); + var src = Gst.ElementFactory.make("videotestsrc", null); + pipeline.add(src); + Gst.Video.Sink sink = (Gst.Video.Sink) Gst.ElementFactory.make("gtksink", null); + Gtk.Widget widget; + sink.get("widget", out widget); + widget.unparent(); + pipeline.add(sink); + src.link(sink); + widget.visible = true; + + pipeline.set_state(Gst.State.PLAYING); + + sink.get_static_pad("sink").notify["caps"].connect(() => { + int width, height; + sink.get_static_pad("sink").caps.get_structure(0).get_int("width", out width); + sink.get_static_pad("sink").caps.get_structure(0).get_int("height", out height); + widget.width_request = width; + widget.height_request = height; + }); + +// call_window.set_participant_video(Xmpp.random_uuid(), widget); + } + + /*private void update_current_video_device(VideoSettingsPopover video_settings_popover) { Xmpp.Xep.JingleRtp.Stream stream = calls.get_video_stream(call); if (stream != null) { video_settings_popover.current_device = call_plugin.get_device(stream, false); } - } + }*/ private void update_own_video() { if (this.call_window.bottom_bar.video_enabled) { @@ -247,8 +369,12 @@ public class Dino.Ui.CallWindowController : Object { } public override void dispose() { + foreach (ulong handler_id in call_window_handler_ids) call_window.disconnect(handler_id); + foreach (ulong handler_id in bottom_bar_handler_ids) call_window.bottom_bar.disconnect(handler_id); + foreach (ulong handler_id in invite_handler_ids) call_window.invite_button.disconnect(handler_id); + + call_window_handler_ids = bottom_bar_handler_ids = invite_handler_ids = new ulong[0]; + base.dispose(); - call.notify["state"].disconnect(on_call_state_changed); - calls.call_terminated.disconnect(on_call_terminated); } } \ No newline at end of file diff --git a/main/src/ui/call_window/participant_widget.vala b/main/src/ui/call_window/participant_widget.vala new file mode 100644 index 00000000..cbf8df2d --- /dev/null +++ b/main/src/ui/call_window/participant_widget.vala @@ -0,0 +1,129 @@ +using Pango; +using Gee; +using Xmpp; +using Dino.Entities; +using Gtk; + +namespace Dino.Ui { + + public class ParticipantWidget : Gtk.Overlay { + + public Widget main_widget; + public Box outer_box = new Box(Orientation.HORIZONTAL, 0) { valign=Align.START, visible=true }; + public Box inner_box = new Box(Orientation.HORIZONTAL, 0) { margin_start=5, margin_top=5, hexpand=true, visible=true }; + public Box title_box = new Box(Orientation.VERTICAL, 0) { valign=Align.CENTER, hexpand=true, visible=true }; + public CallEncryptionButton encryption_button = new CallEncryptionButton() { opacity=0, relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_end=5, visible=true }; + public Label status_label = new Label("") { ellipsize=EllipsizeMode.MIDDLE }; + public Label name_label = new Label("") { ellipsize=EllipsizeMode.MIDDLE, visible=true }; + public Button menu_button = new Button.from_icon_name("view-more-horizontal-symbolic") { relief=ReliefStyle.NONE, visible=true }; + public bool shows_video = false; + public string? participant_name; + + bool is_highest_row = false; + bool is_lowest_row = false; + public bool controls_active { get; set; } + + public ParticipantWidget(string participant_name) { + this.participant_name = participant_name; + name_label.label = participant_name; + + name_label.attributes = new AttrList(); + name_label.attributes.filter((attr) => attr.equal(attr_weight_new(Weight.BOLD))); + + name_label.attributes = new AttrList(); + name_label.attributes.filter((attr) => attr.equal(attr_scale_new(0.9))); + status_label.get_style_context().add_class("dim-label"); + + Util.force_css(outer_box, "* { color: white; text-shadow: 1px 1px black; }"); + menu_button.get_style_context().add_class("participant-title-button"); + encryption_button.get_style_context().add_class("participant-title-button"); + + title_box.add(name_label); + title_box.add(status_label); + + outer_box.add(inner_box); + + inner_box.add(menu_button); + inner_box.add(encryption_button); + inner_box.add(title_box); + inner_box.add(new Button.from_icon_name("go-up-symbolic") { opacity=0, visible=true }); + inner_box.add(new Button.from_icon_name("go-up-symbolic") { opacity=0, visible=true }); + + this.add_overlay(outer_box); + + this.notify["controls-active"].connect(reveal_or_hide_controls); + } + + public void on_show_names_changed(bool show) { + name_label.visible = show; + reveal_or_hide_controls(); + } + + public void on_highest_row_changed(bool is_highest) { + is_highest_row = is_highest; + reveal_or_hide_controls(); + } + + public void on_lowest_row_changed(bool is_lowest) { + is_lowest_row = is_lowest; + reveal_or_hide_controls(); + } + + public void set_video(Widget widget) { + shows_video = true; + widget.visible = true; + set_participant_widget(widget); + } + + public void set_placeholder(Conversation? conversation, StreamInteractor stream_interactor) { + shows_video = false; + Box box = new Box(Orientation.HORIZONTAL, 0) { visible=true }; + box.get_style_context().add_class("video-placeholder-box"); + AvatarImage avatar = new AvatarImage() { allow_gray=false, hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100, visible=true }; + if (conversation != null) { + avatar.set_conversation(stream_interactor, conversation); + } else { + avatar.set_text("?", false); + } + box.add(avatar); + + set_participant_widget(box); + } + + private void set_participant_widget(Widget widget) { + widget.expand = true; + if (main_widget != null) this.remove(main_widget); + main_widget = widget; + this.add(main_widget); + } + + public void set_status(string state) { + status_label.visible = true; + + if (state == "requested") { + status_label.label = _("Calling…"); + } else if (state == "ringing") { + status_label.label = _("Ringing…"); + } else if (state == "establishing") { + status_label.label = _("Connecting…"); + } else { + status_label.visible = false; + } + } + + private void reveal_or_hide_controls() { + if (controls_active && name_label.visible) { + title_box.opacity = 1; + menu_button.opacity = 1; + } else { + title_box.opacity = 0; + menu_button.opacity = 0; + } + if (is_highest_row && controls_active) { + outer_box.get_style_context().add_class("call-header-bar"); + } else { + outer_box.get_style_context().remove_class("call-header-bar"); + } + } + } +} \ No newline at end of file diff --git a/main/src/ui/contact_details/muc_config_form_provider.vala b/main/src/ui/contact_details/muc_config_form_provider.vala index f9f8d7e9..5b4184c5 100644 --- a/main/src/ui/contact_details/muc_config_form_provider.vala +++ b/main/src/ui/contact_details/muc_config_form_provider.vala @@ -33,7 +33,7 @@ public class MucConfigFormProvider : Plugins.ContactDetailsProvider, Object { contact_details.save.connect(() => { // Only send the config form if something was changed if (config_backup != data_form.stanza_node.to_string()) { - stream_interactor.get_module(MucManager.IDENTITY).set_config_form(conversation.account, conversation.counterpart, data_form); + stream_interactor.get_module(MucManager.IDENTITY).set_config_form.begin(conversation.account, conversation.counterpart, data_form); } }); }); diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index 62156761..aad1e6d8 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -2,6 +2,7 @@ using Gee; using Gdk; using Gtk; using Pango; +using Xmpp; using Dino.Entities; @@ -18,7 +19,8 @@ namespace Dino.Ui { public override Object? get_widget(Plugins.WidgetType type) { CallItem call_item = content_item as CallItem; - return new CallWidget(stream_interactor, call_item.call, call_item.conversation) { visible=true }; + CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call_item.call]; + return new CallWidget(stream_interactor, call_item.call, call_state, call_item.conversation) { visible=true }; } public override Gee.List? get_item_actions(Plugins.WidgetType type) { return null; } @@ -31,10 +33,14 @@ namespace Dino.Ui { [GtkChild] public unowned Label title_label; [GtkChild] public unowned Label subtitle_label; [GtkChild] public unowned Revealer incoming_call_revealer; + [GtkChild] public unowned Box outer_additional_box; + [GtkChild] public unowned Box incoming_call_box; + [GtkChild] public unowned Box multiparty_peer_box; [GtkChild] public unowned Button accept_call_button; [GtkChild] public unowned Button reject_call_button; private StreamInteractor stream_interactor; + private CallState call_manager; private Call call; private Conversation conversation; public Call.State call_state { get; set; } // needs to be public for binding @@ -45,8 +51,10 @@ namespace Dino.Ui { size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH; } - public CallWidget(StreamInteractor stream_interactor, Call call, Conversation conversation) { + /** @param call_state Null if it's an old call and we can't interact with it anymore */ + public CallWidget(StreamInteractor stream_interactor, Call call, CallState? call_state, Conversation conversation) { this.stream_interactor = stream_interactor; + this.call_manager = call_state; this.call = call; this.conversation = conversation; @@ -57,36 +65,63 @@ namespace Dino.Ui { }); call.bind_property("state", this, "call-state"); - this.notify["call-state"].connect(update_widget); + this.notify["call-state"].connect(update_call_state); + + if (call_manager != null && (call.state == Call.State.ESTABLISHING || call.state == Call.State.IN_PROGRESS)) { + call_manager.peer_joined.connect(update_counterparts); + } accept_call_button.clicked.connect(() => { - stream_interactor.get_module(Calls.IDENTITY).accept_call(call); + call_manager.accept(); var call_window = new CallWindow(); - call_window.controller = new CallWindowController(call_window, call, stream_interactor); + call_window.controller = new CallWindowController(call_window, call_state, stream_interactor); call_window.present(); }); - reject_call_button.clicked.connect(() => { - stream_interactor.get_module(Calls.IDENTITY).reject_call(call); - }); + reject_call_button.clicked.connect(call_manager.reject); + + update_call_state(); + } + + private void update_counterparts() { + if (call.state != Call.State.IN_PROGRESS && call.state != Call.State.ENDED) return; + if (call.counterparts.size <= 1) return; - update_widget(); + multiparty_peer_box.foreach((widget) => { multiparty_peer_box.remove(widget); }); + + foreach (Jid counterpart in call.counterparts) { + AvatarImage image = new AvatarImage() { force_gray=true, margin_top=2, visible=true }; + image.set_conversation_participant(stream_interactor, conversation, counterpart.bare_jid); + multiparty_peer_box.add(image); + } + AvatarImage image2 = new AvatarImage() { force_gray=true, margin_top=2, visible=true }; + image2.set_conversation_participant(stream_interactor, conversation, call.account.bare_jid); + multiparty_peer_box.add(image2); + + outer_additional_box.get_style_context().add_class("multiparty-participants"); + + multiparty_peer_box.visible = true; + incoming_call_box.visible = false; + incoming_call_revealer.reveal_child = true; } - private void update_widget() { + private void update_call_state() { incoming_call_revealer.reveal_child = false; incoming_call_revealer.get_style_context().remove_class("incoming"); + outer_additional_box.get_style_context().remove_class("incoming-call-box"); switch (call.state) { case Call.State.RINGING: image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR); if (call.direction == Call.DIRECTION_INCOMING) { - bool video = stream_interactor.get_module(Calls.IDENTITY).should_we_send_video(call); + bool video = call_manager.should_we_send_video(); title_label.label = video ? _("Incoming video call") : _("Incoming call"); subtitle_label.label = "Ring ring…!"; + incoming_call_box.visible = true; incoming_call_revealer.reveal_child = true; incoming_call_revealer.get_style_context().add_class("incoming"); + outer_additional_box.get_style_context().add_class("incoming-call-box"); } else { title_label.label = _("Calling…"); subtitle_label.label = "Ring ring…?"; @@ -100,9 +135,11 @@ namespace Dino.Ui { subtitle_label.label = _("Started %s ago").printf(duration); time_update_handler_id = Timeout.add_seconds(get_next_time_change() + 1, () => { - Source.remove(time_update_handler_id); - time_update_handler_id = 0; - update_widget(); + if (time_update_handler_id != 0) { + Source.remove(time_update_handler_id); + time_update_handler_id = 0; + update_call_state(); + } return true; }); @@ -128,7 +165,7 @@ namespace Dino.Ui { if (call.direction == Call.DIRECTION_INCOMING) { subtitle_label.label = _("You missed this call"); } else { - string who = Util.get_participant_display_name(stream_interactor, conversation, call.to); + string who = Util.get_conversation_display_name(stream_interactor, conversation); subtitle_label.label = _("%s missed this call").printf(who); } break; @@ -138,7 +175,7 @@ namespace Dino.Ui { if (call.direction == Call.DIRECTION_INCOMING) { subtitle_label.label = _("You declined this call"); } else { - string who = Util.get_participant_display_name(stream_interactor, conversation, call.to); + string who = Util.get_conversation_display_name(stream_interactor, conversation); subtitle_label.label = _("%s declined this call").printf(who); } break; @@ -148,6 +185,8 @@ namespace Dino.Ui { subtitle_label.label = "Call failed to establish"; break; } + + update_counterparts(); } private string get_duration_string(TimeSpan duration) { @@ -201,6 +240,9 @@ namespace Dino.Ui { Source.remove(time_update_handler_id); time_update_handler_id = 0; } + if (call_manager != null) { + call_manager.peer_joined.disconnect(update_counterparts); + } } } } diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index b356d8cc..73f7c671 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -114,7 +114,6 @@ public class ConversationItemSkeleton : EventBox { [GtkTemplate (ui = "/im/dino/Dino/conversation_content_view/item_metadata_header.ui")] public class ItemMetaDataHeader : Box { [GtkChild] public unowned Label name_label; - [GtkChild] public unowned Label dot_label; [GtkChild] public unowned Label time_label; public Image received_image = new Image() { opacity=0.4 }; public Widget? encryption_image = null; diff --git a/main/src/ui/conversation_titlebar/call_entry.vala b/main/src/ui/conversation_titlebar/call_entry.vala index 4bf946d2..3b3a5b39 100644 --- a/main/src/ui/conversation_titlebar/call_entry.vala +++ b/main/src/ui/conversation_titlebar/call_entry.vala @@ -47,16 +47,16 @@ namespace Dino.Ui { Box box = new Box(Orientation.VERTICAL, 0) { margin=10, visible=true }; audio_button.clicked.connect(() => { stream_interactor.get_module(Calls.IDENTITY).initiate_call.begin(conversation, false, (_, res) => { - Call call = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res); - open_call_window(call); + CallState call_state = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res); + open_call_window(call_state); }); }); box.add(audio_button); video_button.clicked.connect(() => { stream_interactor.get_module(Calls.IDENTITY).initiate_call.begin(conversation, true, (_, res) => { - Call call = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res); - open_call_window(call); + CallState call_state = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res); + open_call_window(call_state); }); }); box.add(video_button); @@ -68,7 +68,7 @@ namespace Dino.Ui { popover_menu.visible = true; }); - stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect((call, conversation) => { + stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect((call, state,conversation) => { update_button_state(); }); @@ -76,6 +76,7 @@ namespace Dino.Ui { update_button_state(); }); stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect((jid, account) => { + if (this.conversation == null) return; if (this.conversation.counterpart.equals_bare(jid) && this.conversation.account.equals(account)) { update_visibility.begin(); } @@ -83,11 +84,14 @@ namespace Dino.Ui { stream_interactor.connection_manager.connection_state_changed.connect((account, state) => { update_visibility.begin(); }); + Dino.Application.get_default().plugin_registry.video_call_plugin.devices_changed.connect((media, incoming) => { + update_visibility.begin(); + }); } - private void open_call_window(Call call) { + private void open_call_window(CallState call_state) { var call_window = new CallWindow(); - var call_controller = new CallWindowController(call_window, call, stream_interactor); + var call_controller = new CallWindowController(call_window, call_state, stream_interactor); call_window.controller = call_controller; call_window.present(); @@ -102,17 +106,15 @@ namespace Dino.Ui { } private void update_button_state() { - Jid? call_counterpart = stream_interactor.get_module(Calls.IDENTITY).is_call_in_progress(); - this.sensitive = call_counterpart == null; - - if (call_counterpart != null && call_counterpart.equals_bare(conversation.counterpart)) { - this.set_image(new Gtk.Image.from_icon_name("dino-phone-in-talk-symbolic", Gtk.IconSize.MENU) { visible=true }); - } else { - this.set_image(new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true }); - } + this.sensitive = !stream_interactor.get_module(Calls.IDENTITY).is_call_in_progress(); } private async void update_visibility() { + if (conversation == null) { + visible = false; + 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); diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index 48313be3..83e9bf57 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -123,6 +123,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { } public async void notify_call(Call call, Conversation conversation, bool video, string conversation_display_name) { + debug("[%s] Call notification", call.account.bare_jid.to_string()); string summary = Markup.escape_text(conversation_display_name); string body = video ? _("Incoming video call") : _("Incoming call"); @@ -140,7 +141,7 @@ 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("deny-call", new Variant.int32(call.id)); + GLib.Application.get_default().activate_action("reject-call", new Variant.int32(call.id)); }); add_action_listener(notification_id, "accept", () => { GLib.Application.get_default().activate_action("accept-call", new Variant.int32(call.id)); diff --git a/main/src/ui/notifier_gnotifications.vala b/main/src/ui/notifier_gnotifications.vala index 5fd3be4b..665d47c4 100644 --- a/main/src/ui/notifier_gnotifications.vala +++ b/main/src/ui/notifier_gnotifications.vala @@ -74,7 +74,7 @@ namespace Dino.Ui { notification.set_icon(new ThemedIcon.from_names(new string[] {"call-start-symbolic"})); notification.set_default_action_and_target_value("app.open-conversation", new Variant.int32(conversation.id)); - notification.add_button_with_target_value(_("Deny"), "app.deny-call", new Variant.int32(call.id)); + notification.add_button_with_target_value(_("Reject"), "app.reject-call", new Variant.int32(call.id)); notification.add_button_with_target_value(_("Accept"), "app.accept-call", new Variant.int32(call.id)); GLib.Application.get_default().send_notification(call.id.to_string(), notification); -- cgit v1.2.3-54-g00ecf From e205743f0cb71e32eecf183d067a4f34a7a89d48 Mon Sep 17 00:00:00 2001 From: fiaxh 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 'main') 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 = "%lu 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 = "%lu 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 = "%lu 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 = "%lu 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-54-g00ecf From 2b3d150949fe1b3c4107e497be7dac8e2ba734aa Mon Sep 17 00:00:00 2001 From: fiaxh 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 'main') 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("Audio") { 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 = "%lu 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 = "%lu 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 = "%lu 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 = "%lu 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("Video") { 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("%s".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 = "%u kbps".printf(info.target_receive_bytes); + target_send_bitrate.label = "%u 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 = "%lu kbps".printf(audio_sent_kbps); + ulong audio_recv_kbps = (info.bytes_received - prev_info.bytes_received) * 8 / 1000; + recv_bps.label = "%lu 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 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 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 descriptions = new ArrayList(); -- cgit v1.2.3-54-g00ecf From 78bb2bbddaf587de77f67c404e8ed5083f16bf8a Mon Sep 17 00:00:00 2001 From: fiaxh 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 'main') 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 peers = new HashMap(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 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(); peers_cpy.add_all(peers.values); @@ -87,6 +113,10 @@ public class Dino.CallState : Object { 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); + } + 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 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 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 IDENTITY = new ModuleIdentity(NS_URI, "muji_meta"); - public signal void call_proposed(Jid from, Jid to, Jid muc_jid, Gee.List 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 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 descriptions = new ArrayList(); @@ -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-54-g00ecf From f0c7dd0682fec8d72c644d8e54896de7bdc40ddb Mon Sep 17 00:00:00 2001 From: fiaxh 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 'main') 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 counterparts = new Gee.ArrayList(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 @@ 10 - 5 + 7 True 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 participant_widgets = new HashMap(); private ArrayList participants = new ArrayList(); 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-54-g00ecf