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;
+ }
+ });
}
}