diff --git a/data/icons/actions/camera-focus-symbolic.svg b/data/icons/actions/camera-focus-symbolic.svg new file mode 100644 index 0000000..0f126e7 --- /dev/null +++ b/data/icons/actions/camera-focus-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/actions/camera-photo-symbolic.svg b/data/icons/actions/camera-photo-symbolic.svg new file mode 100644 index 0000000..ad32b0d --- /dev/null +++ b/data/icons/actions/camera-photo-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/data/icons/actions/cameras-symbolic.svg b/data/icons/actions/cameras-symbolic.svg new file mode 100644 index 0000000..830612d --- /dev/null +++ b/data/icons/actions/cameras-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/actions/encoder-knob-symbolic.svg b/data/icons/actions/encoder-knob-symbolic.svg new file mode 100644 index 0000000..dc997cb --- /dev/null +++ b/data/icons/actions/encoder-knob-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/actions/pick-camera-alt2-symbolic.svg b/data/icons/actions/pick-camera-alt2-symbolic.svg new file mode 100644 index 0000000..f75eef8 --- /dev/null +++ b/data/icons/actions/pick-camera-alt2-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/actions/video-camera-symbolic.svg b/data/icons/actions/video-camera-symbolic.svg new file mode 100644 index 0000000..623bf8e --- /dev/null +++ b/data/icons/actions/video-camera-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/actions/video-decode-symbolic.svg b/data/icons/actions/video-decode-symbolic.svg new file mode 100644 index 0000000..ab1c808 --- /dev/null +++ b/data/icons/actions/video-decode-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/icons.gresource.xml b/data/icons/icons.gresource.xml new file mode 100644 index 0000000..7d4a3f8 --- /dev/null +++ b/data/icons/icons.gresource.xml @@ -0,0 +1,12 @@ + + + + actions/camera-focus-symbolic.svg + actions/cameras-symbolic.svg + actions/video-decode-symbolic.svg + actions/video-camera-symbolic.svg + actions/camera-photo-symbolic.svg + actions/pick-camera-alt2-symbolic.svg + actions/encoder-knob-symbolic.svg + + diff --git a/data/icons/meson.build b/data/icons/meson.build index b69d5db..c2a863e 100644 --- a/data/icons/meson.build +++ b/data/icons/meson.build @@ -11,3 +11,9 @@ install_data( symbolic_dir / ('@0@-symbolic.svg').format(application_id), install_dir: get_option('datadir') / 'icons' / symbolic_dir, ) + +icons = gnome.compile_resources( + 'icons-resources', + 'icons.gresource.xml', + c_name: 'icons', +) diff --git a/meson.build b/meson.build index 9ce23b8..7fab610 100644 --- a/meson.build +++ b/meson.build @@ -12,6 +12,7 @@ project( i18n = import('i18n') gnome = import('gnome') valac = meson.get_compiler('vala') +cc = meson.get_compiler('c') srcdir = meson.project_source_root() / 'src' diff --git a/src/application.vala b/src/application.vala index 1211029..79e22ec 100644 --- a/src/application.vala +++ b/src/application.vala @@ -41,6 +41,11 @@ public class EyeNeko.Application : Adw.Application { base.activate (); var win = this.active_window ?? new EyeNeko.Window (this); win.present (); + var styling = new Gtk.CssProvider (); + styling.load_from_resource ("/io/gitlab/nekocwd/eyeneko/style.css"); + Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (), + styling, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); } private void on_about_action () { diff --git a/src/auto_focus.vala b/src/auto_focus.vala new file mode 100644 index 0000000..1556581 --- /dev/null +++ b/src/auto_focus.vala @@ -0,0 +1,87 @@ +public class AutoFocus : Object { + void set_camfocus (double pos) { + PipeTap.instance.focus = (int) (pos * 10); + } + + private double best_score { get; set; default = 0; } + private double best_lens_pos { get; set; default = 0; } + public double prev_pos { get; private set; default = 0; } + private double step { get; set; default = 10; } + private double max_pos { get; set; default = 100; } + private bool focus_in_process { get; set; default = false; } + private int fc { get; set; default = 0; } + + public void reset_focus () { + best_score = 0; + best_lens_pos = 0; + prev_pos = 0; + step = 10; + max_pos = 100; + focus_in_process = true; + fc = 0; + set_camfocus (0); + } + + void set_diff (double diff) { + max_pos = double.min (best_lens_pos + diff, 100); + prev_pos = double.max (best_lens_pos - diff, 0); + } + + public void adjust_step (double prev_score) { + // message ("%lf < %lf < %lf : %lf %lf / %lf", best_lens_pos, prev_pos, max_pos, step, prev_score, best_score); + if (fc < Environment.get_variable ("FRAME_DELAY").to_int ()) { + fc++; + return; + } else + fc = 0; + if (prev_pos >= max_pos) { + message ("Best focus %lf", best_lens_pos); + switch ((int) (step * 10)) { + case 100: + message ("meow on 10"); + step = 5; + set_diff (10); + set_camfocus (prev_pos); + break; + case 50: + message ("meow on 5"); + step = 2; + set_diff (5); + set_camfocus (prev_pos); + break; + case 20: + message ("meow on 2"); + step = 0.1; + set_diff (2); + set_camfocus (prev_pos); + break; + case 1: + message ("meow on 0.1"); + step = 10; + max_pos = 100; + focus_in_process = false; + if (prev_pos != best_lens_pos) { + prev_pos = best_lens_pos; + set_camfocus (best_lens_pos); + } + break; + default: + message ("meow on def?"); + step = 10; + break; + } + message ("min %lf max %lf step %lf", prev_pos, max_pos, step); + return; + } + + if (!focus_in_process) + return; + if (prev_score > best_score) { + best_score = prev_score; + best_lens_pos = prev_pos; + message ("%lf better than %lf", prev_pos, best_lens_pos); + } + prev_pos += step; + set_camfocus (prev_pos); + } +} diff --git a/src/eyeneko.gresource.xml b/src/eyeneko.gresource.xml index 9cbeac4..00c0476 100644 --- a/src/eyeneko.gresource.xml +++ b/src/eyeneko.gresource.xml @@ -3,5 +3,7 @@ window.ui gtk/help-overlay.ui + style.css + shaders/ccm.glsl diff --git a/src/gst.vala b/src/gst.vala new file mode 100644 index 0000000..88cd7a3 --- /dev/null +++ b/src/gst.vala @@ -0,0 +1,222 @@ + +public class EyeNeko.Gstreamer : Object { + public Gst.Element viewfinder; + private Gst.Element camerabin; + public enum CameraBinMode { + VIDEO = 2, + PHOTO = 1, + } + public bool ready { get; set; default = false; } + public CameraBinMode camerabin_mode { get; set; default = CameraBinMode.PHOTO; } + + Gst.Caps get_best_caps (Gst.Device device) { + int max_w = 0; + int max_h = 0; + float max_fps = 0; + string max_format = ""; + + Gst.Structure max_caps = null; + var caps = device.get_caps (); + for (uint i = 0; i < caps.get_size (); i++) { + var format = caps.get_structure (i).get_name (); + int num, denom, width, height; + if (caps.get_structure (i).get_field_type ("width") != typeof (int) || caps.get_structure (i).get_field_type ("height") != typeof (int)) { + warning ("Can't find best caps :(. I hate arrays!"); + } + caps.get_structure (i).get_int ("width", out width); + caps.get_structure (i).get_int ("height", out height); + caps.get_structure (i).get_fraction ("framerate", out num, out denom); + if (denom == 0) + denom = 1; + var fps = num / denom; + if (max_caps == null || (width > max_w && height > max_h) || (width == max_w && height == max_h && fps > max_fps)) { + max_w = width; + max_h = height; + max_caps = caps.get_structure (i).copy (); + max_fps = fps; + max_format = format; + } + } + var best_cap = new Gst.Caps.empty (); + best_cap.append_structure (max_caps.copy ()); + message ("%s %dx%d %f (%s)", max_format, max_w, max_h, max_fps, best_cap.to_string ()); + return best_cap; + } + + Gst.Caps get_downscale_caps (Gst.Device device, int max_size) { + int w = 0, h = 0; + var caps = get_best_caps (device); + var struct = caps.get_structure (0).copy (); + struct.get_int ("width", out w); + struct.get_int ("height", out h); + struct = new Gst.Structure.empty ("video/x-raw"); + var max_dim = int.max (w, h); + double mult = 1; + if (max_dim > max_size) { + mult = max_size / (double) max_dim; + } + struct.set ("width", typeof (int), (int) (w * mult)); + struct.set ("height", typeof (int), (int) (h * mult)); + + caps = new Gst.Caps.empty (); + caps.append_structure (struct.copy ()); + return caps; + } + + public class Camera : Object { + public string name { owned get { return device.display_name; } } + public Gst.Device device { get; } + + private Camera (Gst.Device device) { + _device = device; + } + + private static Camera[] cameras = null; + public static Camera[] get_all () { + if (cameras == null) { + cameras = new Camera[0]; + var pwprovider = Gst.DeviceProviderFactory.get_by_name ("pipewiredeviceprovider"); + pwprovider.start (); + foreach (var device in pwprovider.get_devices ()) { + if (device.has_classes ("Video/Source")) { + cameras += new Camera (device); + message ("Camera appended %s:", device.display_name); + } + } + } + return cameras; + } + } + + public void start_capture () { + Signal.emit_by_name (camerabin, "start-capture"); + } + + public void stop_capture () { + Signal.emit_by_name (camerabin, "stop-capture"); + } + + public void set_video_filter (Gst.Caps caps) { + var filter = new Gst.Bin ("VideoFilter"); + var videoscale = Gst.ElementFactory.make ("videoconvertscale"); + var capsfilt = Gst.ElementFactory.make ("capsfilter"); + capsfilt.set_property ("caps", caps); + filter.add_many (videoscale, capsfilt); + videoscale.link_many (capsfilt); + add_pads_to_bin (ref filter); + camerabin.set_property ("video-filter", filter); + } + + public void start_stream_from (Camera camera) { + var camerasrc = new Gst.Bin ("CameraSRC"); + + var src = camera.device.create_element ("camerasrc"); + var caps = get_best_caps (camera.device); + var capsfilt = Gst.ElementFactory.make ("capsfilter"); + capsfilt.set_property ("caps", caps); + + var db = Gst.ElementFactory.make ("decodebin3"); + var identity = Gst.ElementFactory.make ("identity"); + + camerasrc.add_many (src, capsfilt, db, identity); + src.link_many (capsfilt, db); + db.pad_added.connect ((pad) => { + if (pad.get_stream () != null && pad.get_stream ().stream_type == Gst.StreamType.VIDEO) { + pad.link (identity.get_static_pad ("sink")); + } + }); + camerasrc.add_pad (new Gst.GhostPad ("src", identity.get_static_pad ("src"))); + + var camerasrc_wrapper = Gst.ElementFactory.make ("wrappercamerabinsrc"); + camerasrc_wrapper.set_property ("video-source", camerasrc); + camerasrc_wrapper.set_property ("video-source-filter", new Logic.Filters.CCM ("CCM", true, true).element); + + camerabin.set_property ("camera-source", camerasrc_wrapper); + set_video_filter (get_downscale_caps (camera.device, 600)); + + camerabin.set_state (Gst.State.NULL); + + camerabin.set_state (Gst.State.PLAYING); + Gst.Debug.bin_to_dot_file ((Gst.Bin) camerabin, Gst.DebugGraphDetails.ALL, camera.name); + } + + private void add_enc_profile () { + var cp = new Gst.PbUtils.EncodingContainerProfile (null, null, Gst.Caps.from_string ("video/quicktime"), null); + var vp = new Gst.PbUtils.EncodingVideoProfile (Gst.Caps.from_string ("video/x-h264"), "Zero Latency", null, 0); + var ap = new Gst.PbUtils.EncodingAudioProfile (Gst.Caps.from_string ("audio/x-ac3"), null, null, 0); + cp.add_profile (vp); + cp.add_profile (ap); + camerabin.set_property ("video-profile", cp); + } + + public void init (ref weak string[] args) { + Gst.init (ref args); + viewfinder = Gst.ElementFactory.make ("gtk4paintablesink", "paintablesink"); + + camerabin = Gst.ElementFactory.make ("camerabin", "camerabin"); + camerabin.set_property ("viewfinder-sink", viewfinder); + camerabin.bind_property ("idle", this, "ready", BindingFlags.SYNC_CREATE); + this.bind_property ("camerabin-mode", camerabin, "mode"); + add_enc_profile (); + camerabin.bus.add_watch (0, bus_callback); + } + + public static void add_pads_to_bin (ref Gst.Bin bin, bool src = true, bool sink = true) { + if (src) + bin.add_pad (new Gst.GhostPad ("src", ((Gst.Element) bin.get_child_by_index (0)).get_static_pad ("src"))); + if (sink) + bin.add_pad (new Gst.GhostPad ("sink", ((Gst.Element) bin.get_child_by_index (bin.get_children_count () - 1)).get_static_pad ("sink"))); + } + + private bool bus_callback (Gst.Bus bus, Gst.Message message) { + switch (message.type) { + case Gst.MessageType.ERROR: + GLib.Error err; + string debug; + message.parse_error (out err, out debug); + GLib.warning ("Error: %s", err.message); + break; + case Gst.MessageType.EOS: + GLib.warning ("end of stream\n"); + break; + case Gst.MessageType.STATE_CHANGED: + Gst.State oldstate; + Gst.State newstate; + Gst.State pending; + message.parse_state_changed (out oldstate, out newstate, + out pending); + // GLib.message ("state changed: %s->%s:%s\n", + // oldstate.to_string (), newstate.to_string (), + // pending.to_string ()); + break; + case Gst.MessageType.TAG: + Gst.TagList tag_list; + stdout.printf ("taglist found\n"); + message.parse_tag (out tag_list); + break; + case Gst.MessageType.ELEMENT: + if (message.get_structure ().get_name () == "GstVideoAnalyse") { + double val; + message.get_structure ().get_double ("luma-average", out val); + // af.adjust_step (val); + } + break; + default: + break; + } + + return true; + } + + private Gstreamer () { + } + + private static Gstreamer _instance = null; + public static Gstreamer instance { + get { + if (_instance == null) + _instance = new Gstreamer (); + return _instance; + } + } +} diff --git a/src/icons/camera-focus-symbolic.svg b/src/icons/camera-focus-symbolic.svg new file mode 100644 index 0000000..0f126e7 --- /dev/null +++ b/src/icons/camera-focus-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/src/logic/color_correction_filter.vala b/src/logic/color_correction_filter.vala new file mode 100644 index 0000000..93b41ec --- /dev/null +++ b/src/logic/color_correction_filter.vala @@ -0,0 +1,34 @@ +public class EyeNeko.Logic.Filters.CCM : GLFilter { + public float red_in_red { get; set; default = 1.0f; } + public float red_in_green { get; set; default = 0.0f; } + public float red_in_blue { get; set; default = 0.0f; } + + public float green_in_red { get; set; default = 0.0f; } + public float green_in_green { get; set; default = 1.0f; } + public float green_in_blue { get; set; default = 0.0f; } + + public float blue_in_red { get; set; default = 0.0f; } + public float blue_in_green { get; set; default = 0.0f; } + public float blue_in_blue { get; set; default = 1.0f; } + + public CCM (string name = "CCM", bool glupload = false, bool gldownload = false) { + base ("ccm", name, glupload, gldownload); + update_props (); + notify.connect (update_props); + } + + private void update_props () { + Gst.Structure uniforms = new Gst.Structure.empty ("uniforms"); + uniforms.set ("rr", typeof (float), red_in_red); + uniforms.set ("rg", typeof (float), red_in_green); + uniforms.set ("rb", typeof (float), red_in_blue); + uniforms.set ("gr", typeof (float), green_in_red); + uniforms.set ("gg", typeof (float), green_in_green); + uniforms.set ("gb", typeof (float), green_in_blue); + uniforms.set ("br", typeof (float), blue_in_red); + uniforms.set ("bg", typeof (float), blue_in_green); + uniforms.set ("bb", typeof (float), blue_in_blue); + + gl_shader.set_property ("uniforms", uniforms); + } +} diff --git a/src/logic/filter.vala b/src/logic/filter.vala new file mode 100644 index 0000000..9c1a824 --- /dev/null +++ b/src/logic/filter.vala @@ -0,0 +1,43 @@ +namespace EyeNeko.Logic.Filters { + public interface Filter : Object { + public abstract Gst.Element element { get; } + } + public class GLFilter : Filter, Object { + public static string get_shader_source (string name) { + try { + return (string) resources_lookup_data ("/io/gitlab/nekocwd/eyeneko/shaders/" + name + ".glsl", GLib.ResourceLookupFlags.NONE).get_data (); + } catch (Error err) { + warning ("Error during shader lookup. %s", err.message); + return ""; + } + } + + public Gst.Element element { get; } + public Gst.Element gl_shader { get; } + + public GLFilter (string shader_name, string elem_name = "GLShader", bool glupload = false, bool gldownload = false) { + var bin = new Gst.Bin (elem_name); + var gl_upload = Gst.ElementFactory.make ("glupload", @"$(elem_name)-upload"); + var gl_download = Gst.ElementFactory.make ("gldownload", @"$(elem_name)-download"); + _gl_shader = Gst.ElementFactory.make ("glshader", @"$(elem_name)-shader"); + gl_shader.set_property ("fragment", GLFilter.get_shader_source (shader_name)); + if (glupload) { + bin.add (gl_upload); + } + + bin.add (gl_shader); + + if (gldownload) { + bin.add (gl_download); + gl_shader.link (gl_download); + } + + if (glupload) { + gl_upload.link (gl_shader); + } + + EyeNeko.Gstreamer.add_pads_to_bin (ref bin); + _element = bin; + } + } +} diff --git a/src/logic/helpers.vala b/src/logic/helpers.vala new file mode 100644 index 0000000..feb6d48 --- /dev/null +++ b/src/logic/helpers.vala @@ -0,0 +1,11 @@ +namespace EyeNeko { + namespace Enum { + public int value (string enum, string name) { + return ((EnumClass) Type.from_name (enum).class_peek ()).get_value_by_name ((string) name).value; + } + + public string alias (string enum, int value) { + return ((EnumClass) Type.from_name (enum).class_peek ()).get_value (value).value_name; + } + } +} diff --git a/src/main.vala b/src/main.vala index d6b52f6..6ae3858 100644 --- a/src/main.vala +++ b/src/main.vala @@ -23,6 +23,7 @@ int main (string[] args) { Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); Intl.textdomain (Config.GETTEXT_PACKAGE); + EyeNeko.Gstreamer.instance.init (ref args); var app = new EyeNeko.Application (); return app.run (args); } diff --git a/src/meson.build b/src/meson.build index ef4d334..6d60ae1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,13 +1,26 @@ eyeneko_sources = [ + icons, 'main.vala', 'application.vala', 'window.vala', + 'gst.vala', + 'pipetap_proxy.vala', + 'auto_focus.vala', + 'logic/color_correction_filter.vala', + 'logic/filter.vala', + 'logic/helpers.vala', ] +vapi_dir = meson.current_source_dir() / 'vapi' +add_project_arguments(['--vapidir', vapi_dir], language: 'vala') eyeneko_deps = [ config_dep, dependency('gtk4'), dependency('libadwaita-1', version: '>= 1.4'), + dependency('gstreamer-1.0'), + # dependency('gstreamer-pbutils-1.0'), + dependency('gstreamer-pbutils-1.0'), + valac.find_library('encoding-profile-helper', dirs: vapi_dir), ] blueprints = custom_target( diff --git a/src/pipetap_proxy.vala b/src/pipetap_proxy.vala new file mode 100644 index 0000000..3b015eb --- /dev/null +++ b/src/pipetap_proxy.vala @@ -0,0 +1,13 @@ +[DBus (name = "io.gitlab.nekocwd.pipetap1")] +public interface PipeTap : Object { + public abstract int focus { get; set; } + + private static PipeTap _instance = null; + public static PipeTap instance { + get { + if (_instance == null) + _instance = Bus.get_proxy_sync (BusType.SESSION, "io.gitlab.nekocwd.pipetap", "/io/gitlab/nekocwd/pipetap", DBusProxyFlags.NONE); + return _instance; + } + } +} diff --git a/src/shaders/ccm.glsl b/src/shaders/ccm.glsl new file mode 100644 index 0000000..76fea7b --- /dev/null +++ b/src/shaders/ccm.glsl @@ -0,0 +1,29 @@ +#version 100 +#ifdef GL_ES +precision mediump float; +#endif +varying vec2 v_texcoord; +uniform sampler2D image; + +uniform float rr; +uniform float rg; +uniform float rb; + +uniform float gr; +uniform float gg; +uniform float gb; + +uniform float br; +uniform float bg; +uniform float bb; + +void main() { + vec3 color = texture2D(image, v_texcoord).rgb; + mat4 matrix = mat4( + vec4(rr, rg, rb, 0.0), + vec4(gr, gg, gb, 0.0), + vec4(br, bg, bb, 0.0), + vec4(0.0, 0.0, 0.0, 1.0) + ); + gl_FragColor = matrix * vec4(color, 1.0); +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..a94b66d --- /dev/null +++ b/src/style.css @@ -0,0 +1,6 @@ +.capture-btn { + border: 5px solid white; +} +.focus-btn { + color: green; +} diff --git a/src/vapi/encoding-profile-helper.vapi b/src/vapi/encoding-profile-helper.vapi new file mode 100644 index 0000000..b80a070 --- /dev/null +++ b/src/vapi/encoding-profile-helper.vapi @@ -0,0 +1,4 @@ +[CCode (cheader_filename = "gstreamer-1.0/gst/pbutils/encoding-profile.h", lower_case_cprefix = "gst_")] +namespace Gst.EyeNeko { + public Gst.PbUtils.EncodingProfile encoding_profile_from_string (string string); +} diff --git a/src/window.blp b/src/window.blp index 1391611..a56d97b 100644 --- a/src/window.blp +++ b/src/window.blp @@ -2,29 +2,246 @@ using Gtk 4.0; using Adw 1; template $EyeNekoWindow: Adw.ApplicationWindow { + styles [ + "osd", + ] + title: _("EyeNeko"); default-width: 800; default-height: 600; - content: Adw.ToolbarView { - [top] - Adw.HeaderBar { - [end] - MenuButton { - primary: true; - icon-name: "open-menu-symbolic"; - tooltip-text: _("Main Menu"); - menu-model: primary_menu; + content: Gtk.Overlay overlay { + child: Gtk.Picture viewfinder { + halign: fill; + valign: fill; + hexpand: true; + vexpand: true; + }; + }; +} + +Adw.ToolbarView toolbar { + styles [ + "flat", + ] + + [top] + Adw.HeaderBar { + show-title: false; + + [end] + MenuButton { + primary: true; + icon-name: "open-menu-symbolic"; + tooltip-text: _("Main Menu"); + menu-model: primary_menu; + } + } + + [bottom] + ActionBar { + styles [ + "osd", + ] + + [center] + Box { + spacing: 12; + + MenuButton profile_btn { + styles [ + "circular", + "menu", + ] + + Image { + icon-name: "encoder-knob-symbolic"; + pixel-size: 24; + } + + width-request: 40; + height-request: 40; + valign: center; + } + + Button { + valign: center; + halign: end; + width-request: 60; + height-request: 60; + + Box { + halign: center; + valign: center; + orientation: vertical; + + Image { + icon-name: "camera-focus-symbolic"; + pixel-size: 24; + } + + Label focus_label { + label: _("100.0"); + } + } + + styles [ + "circular", + "focus-btn", + ] + } + + Button capture_btn { + width-request: 80; + height-request: 80; + + Image { + icon-name: "emote-love-symbolic"; + pixel-size: 32; + } + + styles [ + "circular", + "capture-btn", + ] + } + + MenuButton video_source_btn { + styles [ + "circular", + "menu", + ] + + Image { + icon-name: "pick-camera-alt2-symbolic"; + pixel-size: 24; + } + + width-request: 40; + height-request: 40; + valign: center; + } + + Adw.ToggleGroup camera_mode { + styles [ + "round", + ] + + valign: center; + orientation: vertical; + active-name: "photo"; + + Adw.Toggle { + icon-name: "camera-photo-symbolic"; + name: "photo"; + } + + Adw.Toggle { + icon-name: "video-camera-symbolic"; + name: "video"; + } } } + } - content: Label label { - label: _("Hello, World!"); + /*CenterBox { + halign: fill; + hexpand: true; + margin-bottom: 0; + margin-end: 0; + margin-start: 0; + height-request: 120; - styles [ - "title-1", - ] - }; + styles [ + "toolbar", + "osd", + ] + + [center] + CenterBox { + [start] + Box { + valign: center; + + MenuButton mode_switch { + width-request: 80; + height-request: 80; + margin-end: 24; + icon-name: "video-decode-symbolic"; + + styles [ + "circular", + "cam-switch-btn", + ] + } + + MenuButton cam_switch { + width-request: 80; + height-request: 80; + margin-end: 24; + icon-name: "cameras-symbolic"; + + styles [ + "circular", + "cam-switch-btn", + ] + } + } + + [center] + Button capture { + valign: center; + halign: center; + width-request: 120; + height-request: 120; + + Image { + icon-name: "emote-love-symbolic"; + pixel-size: 48; + } + + styles [ + "circular", + "capture-btn", + ] + } + + [end] + Button focus { + valign: center; + halign: end; + width-request: 80; + height-request: 80; + margin-start: 24; + + Box { + halign: center; + valign: center; + orientation: vertical; + + Image { + icon-name: "camera-focus-symbolic"; + pixel-size: 32; + } + + Label focus_label { + label: _("100.0"); + } + } + + styles [ + "circular", + "focus-btn", + ] + } + } + }*/ + content: Label label { + label: _("EyeNeko"); + + styles [ + "title-1", + ] }; } diff --git a/src/window.vala b/src/window.vala index 63f67ac..98263c1 100644 --- a/src/window.vala +++ b/src/window.vala @@ -21,9 +21,85 @@ [GtkTemplate (ui = "/io/gitlab/nekocwd/eyeneko/window.ui")] public class EyeNeko.Window : Adw.ApplicationWindow { [GtkChild] - private unowned Gtk.Label label; + private unowned Gtk.Overlay overlay; + [GtkChild] + private unowned Adw.ToolbarView toolbar; + [GtkChild] + private unowned Gtk.Picture viewfinder; + [GtkChild] + private unowned Gtk.MenuButton video_source_btn; + [GtkChild] + private unowned Gtk.Button capture_btn; + [GtkChild] + private unowned Adw.ToggleGroup camera_mode; + + public string camera_path { get; set; default = "Unknown"; } + private void setup_video_source_changer () { + notify["camera-path"].connect (() => { + message ("Camera switching P0: retrived camera path: %s", camera_path); + foreach (var camera in Gstreamer.Camera.get_all ()) { + if (camera.device.get_path_string () == camera_path) { + message ("Camera switching P1: camera found by path: %s", camera.name); + Gstreamer.instance.start_stream_from (camera); + return; + } + } + }); + + if (Gstreamer.Camera.get_all ().length > 0) + camera_path = Gstreamer.Camera.get_all ()[0].device.get_path_string (); + + var change_video_source_action = new PropertyAction ("change-video-source", this, "camera-path"); + add_action (change_video_source_action); + + + + + var sources_menu = new Menu (); + foreach (var camera in Gstreamer.Camera.get_all ()) { + var item = new MenuItem (camera.name, null); + item.set_action_and_target_value ("win.change-video-source", camera.device.get_path_string ()); + sources_menu.append_item (item); + } + + video_source_btn.set_menu_model (sources_menu); + } public Window (Gtk.Application app) { Object (application: app); + overlay.add_overlay (toolbar); + + setup_video_source_changer (); + + Gdk.Paintable paintable; + Gstreamer.instance.viewfinder.get ("paintable", out paintable); + viewfinder.set_paintable (paintable); + Gstreamer.instance.start_stream_from (Gstreamer.Camera.get_all ()[0]); + + + // focus.clicked.connect (Gstreamer.af.reset_focus); + // Gstreamer.af.bind_property ("prev_pos", focus_label, "label", GLib.BindingFlags.SYNC_CREATE, (b, src, ref tgt) => { tgt = "%.1f".printf (src.get_double ()); return true; }); + + capture_btn.clicked.connect (() => { + message ("%d %d", Gstreamer.instance.camerabin_mode, (int) Gstreamer.instance.ready); + if (Gstreamer.instance.camerabin_mode == Gstreamer.CameraBinMode.VIDEO && !Gstreamer.instance.ready) { + Gstreamer.instance.stop_capture (); + } else { + Gstreamer.instance.start_capture (); + } + }); + + camera_mode.notify["active"].connect (() => { + switch (camera_mode.active_name) { + case "photo": + Gstreamer.instance.camerabin_mode = Gstreamer.CameraBinMode.PHOTO; + break; + case "video": + Gstreamer.instance.camerabin_mode = Gstreamer.CameraBinMode.VIDEO; + break; + default: + break; + } + }); } }