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/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 ++++++++ 6 files changed, 596 insertions(+), 242 deletions(-) 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/src/ui/call_window') 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 -- cgit v1.2.3-54-g00ecf