aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/call_window
diff options
context:
space:
mode:
authorfiaxh <git@lightrise.org>2021-11-04 17:35:11 +0100
committerfiaxh <git@lightrise.org>2021-11-10 11:05:34 +0100
commit3dc4d7f1558bb574eb99dade9a05727604e2e2ca (patch)
treea936dede1f2f41a27d7931ee7f668ef85deaa93b /main/src/ui/call_window
parent26d10d1dcb95f11b65611473c9840e13683cb5ec (diff)
downloaddino-3dc4d7f1558bb574eb99dade9a05727604e2e2ca.tar.gz
dino-3dc4d7f1558bb574eb99dade9a05727604e2e2ca.zip
Add (disabled) multi-party call UI
Diffstat (limited to 'main/src/ui/call_window')
-rw-r--r--main/src/ui/call_window/call_bottom_bar.vala11
-rw-r--r--main/src/ui/call_window/call_connection_details_window.vala84
-rw-r--r--main/src/ui/call_window/call_encryption_button.vala20
-rw-r--r--main/src/ui/call_window/call_window.vala262
-rw-r--r--main/src/ui/call_window/call_window_controller.vala332
-rw-r--r--main/src/ui/call_window/participant_widget.vala129
6 files changed, 596 insertions, 242 deletions
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("<b>Audio</b>") { use_markup=true, xalign=0, visible=true }, 0, row_at++, 1, 1);
+ put_row("RTP");
+ grid.attach(audio_rtp_ready, 1, row_at++, 1, 1);
+ put_row("RTCP");
+ grid.attach(audio_rtcp_ready, 1, row_at++, 1, 1);
+ put_row("Sent bp/s");
+ grid.attach(audio_sent_bps, 1, row_at++, 1, 1);
+ put_row("Received bp/s");
+ grid.attach(audio_recv_bps, 1, row_at++, 1, 1);
+ put_row("Codec");
+ grid.attach(audio_codec, 1, row_at++, 1, 1);
+
+ 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("<b>Video</b>") { use_markup=true, xalign=0, visible=true }, 0, row_at++, 1, 1);
+ put_row("RTP");
+ grid.attach(video_rtp_ready, 1, row_at++, 1, 1);
+ put_row("RTCP");
+ grid.attach(video_rtcp_ready, 1, row_at++, 1, 1);
+ put_row("Sent bp/s");
+ grid.attach(video_sent_bps, 1, row_at++, 1, 1);
+ put_row("Received bp/s");
+ grid.attach(video_recv_bps, 1, row_at++, 1, 1);
+ put_row("Codec");
+ grid.attach(video_codec, 1, row_at++, 1, 1);
+
+ 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<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
+ private ArrayList<string> participants = new ArrayList<string>();
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<string> 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<string> first_part = new ArrayList<string>();
+ ArrayList<string> last_part = new ArrayList<string>();
+
+ 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<string, Plugins.VideoCallWidget> participant_videos = new HashMap<string, Plugins.VideoCallWidget>();
+ private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
+ private HashMap<string, PeerState> peer_states = new HashMap<string, PeerState>();
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<Account> acc_list = new ArrayList<Account>(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<string>();
+ 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