From 9084da5697fda4b702367b7f9624001dfb8711c9 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 5 Jun 2022 17:26:58 +0000 Subject: [PATCH 1/5] Use libportal to request access to camera --- io.elementary.camera.yml | 12 +++++- meson.build | 1 + src/Widgets/CameraView.vala | 76 ++++++++++++++++++++++++++++--------- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/io.elementary.camera.yml b/io.elementary.camera.yml index 1116871cd..fd3909310 100644 --- a/io.elementary.camera.yml +++ b/io.elementary.camera.yml @@ -11,7 +11,6 @@ finish-args: - '--socket=fallback-x11' - '--socket=wayland' - '--socket=pulseaudio' - - '--device=all' - '--metadata=X-DConf=migrate-path=/io/elementary/camera/' cleanup: @@ -30,6 +29,17 @@ modules: url: http://git.0pointer.net/clone/libcanberra.git disable-shallow-clone: true + - name: portal + buildsystem: meson + config-opts: + - '-Ddocs=false' + - '-Dtests=false' + - '-Dbackends=gtk3' + sources: + - type: git + url: https://github.com/flatpak/libportal.git + tag: 0.6 + - name: camera buildsystem: meson sources: diff --git a/meson.build b/meson.build index c8ee3446b..3a7ba6473 100644 --- a/meson.build +++ b/meson.build @@ -43,6 +43,7 @@ executable( dependency('gtk+-3.0'), dependency('libcanberra'), dependency('libhandy-1', version: '>=0.90.0'), + dependency('libportal'), meson.get_compiler('vala').find_library('posix') ], install : true diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index 0b3fe200e..095c6be2b 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -20,13 +20,18 @@ * Corentin Noël */ + +errordomain Camera.Permissions { + ACCESS_DENIED +} + public class Camera.Widgets.CameraView : Gtk.Box { private const string VIDEO_SRC_NAME = "v4l2src"; public signal void recording_finished (string file_path); private Gtk.Stack main_widget; private Gtk.Box status_box; - private Granite.Widgets.AlertView no_device_view; + private Granite.Widgets.AlertView device_error_view; private Gtk.Label status_label; Gtk.Widget gst_video_widget; @@ -36,7 +41,7 @@ public class Camera.Widgets.CameraView : Gtk.Box { private Gst.Video.Direction? hflip; private Gst.Bin? record_bin; private Gst.Device? current_device = null; - private uint init_device_timeout_id = 0; + private uint device_init_timeout_id = 0; public uint n_cameras { get { @@ -89,42 +94,79 @@ public class Camera.Widgets.CameraView : Gtk.Box { status_box.pack_start (spinner); status_box.pack_start (status_label); - no_device_view = new Granite.Widgets.AlertView ( - _("No Supported Camera Found"), - _("Connect a webcam or other supported video device to take photos and video."), - "" - ); + device_error_view = new Granite.Widgets.AlertView ("", "",""); main_widget.add (status_box); // must be add_child for GTK4 - main_widget.add (no_device_view); // must be add_child for GTK4 + main_widget.add (device_error_view); // must be add_child for GTK4 monitor.get_bus ().add_watch (GLib.Priority.DEFAULT, on_bus_message); var caps = new Gst.Caps.empty_simple ("video/x-raw"); caps.append (new Gst.Caps.empty_simple ("image/jpeg")); monitor.add_filter ("Video/Source", caps); - init_device_timeout_id = Timeout.add_seconds (2, () => { + if (!Xdp.Portal.running_under_sandbox ()) { + start_device_init_timeout (); + + } else { + var portal = new Xdp.Portal (); + portal.access_camera.begin (null, Xdp.CameraFlags.NONE, null, (obj, res) => { + try { + var accessGranted = portal.access_camera.end (res); + debug ("accessGranted: %s", accessGranted.to_string ()); + + if (!accessGranted) { + throw new Camera.Permissions.ACCESS_DENIED ("Access to camera denied"); + + } else { + start_device_init_timeout (); + var camera_fd = portal.open_pipewire_remote_for_camera (); + debug ("camera_fd: %i", camera_fd); + } + + } catch (Error e) { + warning ("Camera Access Denied: %s", e.message); + + device_error_view.title = _("Camera Access Denied"); + device_error_view.description = _("Allow access to your camera from the privacy settings."); + device_error_view.show (); + main_widget.visible_child = device_error_view; + } + }); + } + } + + private void show_no_device_error () { + device_error_view.title = _("No Supported Camera Found"); + device_error_view.description = _("Connect a webcam or other supported video device to take photos and video."); + device_error_view.show (); + main_widget.visible_child = device_error_view; + } + + private void start_device_init_timeout () { + device_init_timeout_id = Timeout.add_seconds (2, () => { if (n_cameras == 0) { - no_device_view.show (); - main_widget.visible_child = no_device_view; + show_no_device_error (); } return Source.REMOVE; }); } - private void on_camera_added (Gst.Device device) { - if (init_device_timeout_id > 0) { - Source.remove (init_device_timeout_id); - init_device_timeout_id = 0; + private void stop_device_init_timeout () { + if (device_init_timeout_id > 0) { + Source.remove (device_init_timeout_id); + device_init_timeout_id = 0; } + } + + private void on_camera_added (Gst.Device device) { + stop_device_init_timeout (); camera_added (device); change_camera (device); } private void on_camera_removed (Gst.Device device) { camera_removed (device); if (n_cameras == 0) { - no_device_view.show (); - main_widget.visible_child = no_device_view; + show_no_device_error (); } else { change_camera (monitor.get_devices ().nth_data (0)); } From 992f1b9ab905a53beaf0bad7b63aad3ab1913124 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 5 Jun 2022 17:32:58 +0000 Subject: [PATCH 2/5] Make Linter Happy --- src/Widgets/CameraView.vala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index 095c6be2b..b9d51b790 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -21,7 +21,7 @@ */ -errordomain Camera.Permissions { +errordomain Camera.Error { ACCESS_DENIED } @@ -111,11 +111,11 @@ public class Camera.Widgets.CameraView : Gtk.Box { var portal = new Xdp.Portal (); portal.access_camera.begin (null, Xdp.CameraFlags.NONE, null, (obj, res) => { try { - var accessGranted = portal.access_camera.end (res); - debug ("accessGranted: %s", accessGranted.to_string ()); + var access_granted = portal.access_camera.end (res); + debug ("access_granted: %s", access_granted.to_string ()); - if (!accessGranted) { - throw new Camera.Permissions.ACCESS_DENIED ("Access to camera denied"); + if (!access_granted) { + throw new Camera.Error.ACCESS_DENIED ("Access to camera denied"); } else { start_device_init_timeout (); From 82edeb71a0ea571d8e53d739b11142fac4fa00af Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Sun, 5 Jun 2022 17:39:01 +0000 Subject: [PATCH 3/5] Fixed uncatched error due to namespace conflict --- src/Widgets/CameraView.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index b9d51b790..7f4a28173 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -21,7 +21,7 @@ */ -errordomain Camera.Error { +errordomain Camera.PermissionError { ACCESS_DENIED } @@ -115,7 +115,7 @@ public class Camera.Widgets.CameraView : Gtk.Box { debug ("access_granted: %s", access_granted.to_string ()); if (!access_granted) { - throw new Camera.Error.ACCESS_DENIED ("Access to camera denied"); + throw new Camera.PermissionError.ACCESS_DENIED ("Access to camera denied"); } else { start_device_init_timeout (); From edd7979483fdfcf207c280fc2421d1ad33ec17e9 Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Mon, 6 Jun 2022 10:20:55 +0000 Subject: [PATCH 4/5] Experimenting with pipewiresrc --- src/Widgets/CameraView.vala | 74 ++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index 7f4a28173..164e827f7 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -118,9 +118,9 @@ public class Camera.Widgets.CameraView : Gtk.Box { throw new Camera.PermissionError.ACCESS_DENIED ("Access to camera denied"); } else { - start_device_init_timeout (); var camera_fd = portal.open_pipewire_remote_for_camera (); debug ("camera_fd: %i", camera_fd); + create_pipeline_fd (camera_fd); } } catch (Error e) { @@ -192,6 +192,20 @@ public class Camera.Widgets.CameraView : Gtk.Box { message.parse_device_removed (out device); on_camera_removed (device); + break; + case ERROR: + Error error; + string debug_info; + message.parse_error (out error, out debug_info); + warning ("Unexpected error from pipeline: %s", error.message); + + break; + case EOS: + // free resources: + if (pipeline != null) { + pipeline.set_state (Gst.State.NULL); + } + break; default: break; @@ -232,6 +246,62 @@ public class Camera.Widgets.CameraView : Gtk.Box { current_device = camera; } + private void create_pipeline_fd (int camera_fd) { + try { + pipeline = (Gst.Pipeline) Gst.parse_launch ( + "pipewiresrc fd=%i ! videoconvert ! ".printf (camera_fd) + + "decodebin name=decodebin ! " + + "videoflip method=horizontal-flip name=hflip ! " + + "videobalance name=balance ! " + + "tee name=tee ! " + + "videorate name=videorate ! " + + "queue leaky=downstream max-size-buffers=10 ! " + + "videoscale name=videoscale" + ); + + tee = pipeline.get_by_name ("tee"); + hflip = (pipeline.get_by_name ("hflip") as Gst.Video.Direction); + color_balance = (pipeline.get_by_name ("balance") as Gst.Video.ColorBalance); + + if (gst_video_widget != null) { + main_widget.remove (gst_video_widget); + } + + dynamic Gst.Element videorate = pipeline.get_by_name ("videorate"); + videorate.max_rate = 30; + videorate.drop_only = true; + + dynamic Gst.Element gtksink = Gst.ElementFactory.make ("gtkglsink", null); + if (gtksink != null) { + dynamic Gst.Element glsinkbin = Gst.ElementFactory.make ("glsinkbin", null); + glsinkbin.sink = gtksink; + pipeline.add (glsinkbin); + pipeline.get_by_name ("videoscale").link (glsinkbin); + } else { + gtksink = Gst.ElementFactory.make ("gtksink", null); + pipeline.add (gtksink); + pipeline.get_by_name ("videoscale").link (gtksink); + } + + gst_video_widget = gtksink.widget; + + main_widget.add (gst_video_widget); // must be add_child for GTK4 + gst_video_widget.show (); + + main_widget.visible_child = gst_video_widget; + pipeline.set_state (Gst.State.PLAYING); + + pipeline.get_bus ().add_watch (GLib.Priority.DEFAULT, on_bus_message); + + } catch (Error e) { + // It is possible that there is another camera present that could selected so do not show + // no_device_error + var dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Unable To View Camera"), e.message, "dialog-error"); + dialog.run (); + dialog.destroy (); + } + } + private void create_pipeline (Gst.Device camera) { try { var caps = camera.get_caps (); @@ -300,7 +370,7 @@ public class Camera.Widgets.CameraView : Gtk.Box { pipeline.set_state (Gst.State.PLAYING); } catch (Error e) { // It is possible that there is another camera present that could selected so do not show - // no_device_view + // no_device_error var dialog = new Granite.MessageDialog.with_image_from_icon_name (_("Unable To View Camera"), e.message, "dialog-error"); dialog.run (); dialog.destroy (); From 3b12df8b304f51025676c2a7784cd5c8d7ef6a9c Mon Sep 17 00:00:00 2001 From: Marco Betschart Date: Thu, 16 Jun 2022 10:32:07 +0000 Subject: [PATCH 5/5] Using realize signal renders portal dialog on top --- src/Widgets/CameraView.vala | 46 +++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/Widgets/CameraView.vala b/src/Widgets/CameraView.vala index 164e827f7..db105d9e2 100644 --- a/src/Widgets/CameraView.vala +++ b/src/Widgets/CameraView.vala @@ -108,29 +108,31 @@ public class Camera.Widgets.CameraView : Gtk.Box { start_device_init_timeout (); } else { - var portal = new Xdp.Portal (); - portal.access_camera.begin (null, Xdp.CameraFlags.NONE, null, (obj, res) => { - try { - var access_granted = portal.access_camera.end (res); - debug ("access_granted: %s", access_granted.to_string ()); - - if (!access_granted) { - throw new Camera.PermissionError.ACCESS_DENIED ("Access to camera denied"); - - } else { - var camera_fd = portal.open_pipewire_remote_for_camera (); - debug ("camera_fd: %i", camera_fd); - create_pipeline_fd (camera_fd); + realize.connect (() => { + var portal = new Xdp.Portal (); + portal.access_camera.begin (null, Xdp.CameraFlags.NONE, null, (obj, res) => { + try { + var access_granted = portal.access_camera.end (res); + debug ("access_granted: %s", access_granted.to_string ()); + + if (!access_granted) { + throw new Camera.PermissionError.ACCESS_DENIED ("Access to camera denied"); + + } else { + var camera_fd = portal.open_pipewire_remote_for_camera (); + debug ("camera_fd: %i", camera_fd); + create_pipeline_fd (camera_fd); + } + + } catch (Error e) { + warning ("Camera Access Denied: %s", e.message); + + device_error_view.title = _("Camera Access Denied"); + device_error_view.description = _("Allow access to your camera from the privacy settings."); + device_error_view.show (); + main_widget.visible_child = device_error_view; } - - } catch (Error e) { - warning ("Camera Access Denied: %s", e.message); - - device_error_view.title = _("Camera Access Denied"); - device_error_view.description = _("Allow access to your camera from the privacy settings."); - device_error_view.show (); - main_widget.visible_child = device_error_view; - } + }); }); } }