Class: Rubycam::GTK::CameraApp
- Inherits:
-
Object
- Object
- Rubycam::GTK::CameraApp
- Defined in:
- lib/rubycam/gtk/camera_app.rb
Constant Summary collapse
- FRAME_INTERVAL_MS =
16- SLIDER_KEYS =
%i[pan_absolute tilt_absolute zoom_absolute brightness contrast saturation sharpness].freeze
- FULL_SIZE =
[1380, 640].freeze
- COMPACT_SIZE =
[340, 640].freeze
- STATUS_FAILURES_BEFORE_RECONNECT =
After this many consecutive failed polls the camera is treated as gone (a transient EIO during privacy-sleep transitions must not trigger a disruptive reconnect).
3- WATCHDOG_SECONDS =
Longer than the camera's first-frame latency, so warmup after a stream (re)start is never mistaken for a dead stream.
5
Instance Method Summary collapse
- #app ⇒ Object
- #build ⇒ Object
- #close_device ⇒ Object
- #control_value(key) ⇒ Object
-
#device ⇒ Object
Accepts a device path, a /dev name, or a card/bus substring (e.g. 'OBSBOT Tiny 2').
-
#initialize(device_path) ⇒ CameraApp
constructor
A new instance of CameraApp.
- #layout ⇒ Object
- #monotonic_now ⇒ Object
- #obsbot ⇒ Object
- #panel ⇒ Object
- #picture ⇒ Object
- #pump_one_frame ⇒ Object
-
#reconnect_camera ⇒ Object
The camera is gone (unplugged or re-enumerated): drop the dead handle and try to find it again on every poll until it returns.
- #reset_button ⇒ Object
- #reset_controls ⇒ Object
-
#restore_sliders ⇒ Object
When the app started without a camera the sidebar has no sliders yet; build and attach them on first successful (re)connect.
-
#set_control_safely(key, value) ⇒ Object
Control writes can fail transiently (EIO/EBUSY) while the camera enters or leaves privacy sleep; an exception here must never kill the main loop.
- #show_frame(jpeg) ⇒ Object
- #sidebar ⇒ Object
-
#sleep_camera ⇒ Object
Sleep/wake go through OBSBOT's vendor extension unit — the same command its official software sends.
- #slider_for(ctrl) ⇒ Object
- #slider_scale(key) ⇒ Object
- #sliders ⇒ Object
- #start_frame_pump ⇒ Object
- #start_status_sync ⇒ Object
-
#sync_status ⇒ Object
Poll the camera and mirror its state into the panel, so the UI follows changes made outside the app (folding the camera down, other software).
-
#toggle_compact ⇒ Object
Dashboard ⇄ compact widget mode: compact hides the preview and the V4L2 sliders, leaving just the OBSBOT panel (like Tiny4Linux's widget mode).
- #wake_camera ⇒ Object
- #window ⇒ Object
Constructor Details
#initialize(device_path) ⇒ CameraApp
Returns a new instance of CameraApp.
19 20 21 22 |
# File 'lib/rubycam/gtk/camera_app.rb', line 19 def initialize(device_path) @device_path = device_path @awake = true end |
Instance Method Details
#app ⇒ Object
78 |
# File 'lib/rubycam/gtk/camera_app.rb', line 78 def app = @app ||= Gtk::Application.new('org.rubycam.viewer', :default_flags) |
#build ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/rubycam/gtk/camera_app.rb', line 24 def build app.tap do app.signal_connect('activate') do app.add_window(window) window.tap do |win| win.title = begin device.card rescue SystemCallError 'Rubycam (no camera)' end win.set_default_size(*FULL_SIZE) win.child = layout win.signal_connect('close-request') do GLib::Source.remove(@pump) if @pump close_device false end end layout.tap do |l| l.append(picture) l.append() l.append(panel.build) .tap do |side| .each_value { |s| side.append(s) } side.append() @sliders_in_sidebar = !.empty? .tap do |btn| btn.signal_connect('clicked') { reset_controls } end end end start_frame_pump start_status_sync window.present end end end |
#close_device ⇒ Object
207 208 209 210 211 212 213 214 |
# File 'lib/rubycam/gtk/camera_app.rb', line 207 def close_device @device&.close rescue SystemCallError nil ensure @device = nil @obsbot = nil end |
#control_value(key) ⇒ Object
237 238 239 240 241 |
# File 'lib/rubycam/gtk/camera_app.rb', line 237 def control_value(key) device.controls[key].value rescue SystemCallError nil end |
#device ⇒ Object
Accepts a device path, a /dev name, or a card/bus substring (e.g. 'OBSBOT Tiny 2').
70 71 72 73 74 75 76 |
# File 'lib/rubycam/gtk/camera_app.rb', line 70 def device @device ||= (Rubycam::Device.find(@device_path) or raise Errno::ENOENT, @device_path).tap do |cam| cam.set_format(width: 1280, height: 720, pixel_format: 'MJPG') cam.set_fps(30) end end |
#layout ⇒ Object
80 |
# File 'lib/rubycam/gtk/camera_app.rb', line 80 def layout = @layout ||= Gtk::Box.new(:horizontal, 12) |
#monotonic_now ⇒ Object
280 |
# File 'lib/rubycam/gtk/camera_app.rb', line 280 def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC) |
#obsbot ⇒ Object
137 |
# File 'lib/rubycam/gtk/camera_app.rb', line 137 def obsbot = @obsbot ||= Rubycam::Obsbot.new(device) |
#panel ⇒ Object
82 83 84 85 86 87 |
# File 'lib/rubycam/gtk/camera_app.rb', line 82 def panel @panel ||= Rubycam::GTK::ObsbotPanel.new(bot: -> { obsbot }, on_wake: -> { wake_camera }, on_sleep: -> { sleep_camera }, on_compact_toggle: -> { toggle_compact }) end |
#picture ⇒ Object
89 90 91 92 93 94 95 96 97 |
# File 'lib/rubycam/gtk/camera_app.rb', line 89 def picture @picture ||= Gtk::Picture.new.tap do |pic| pic. = true pic. = true pic.margin_start = 12 pic.margin_top = 12 pic.margin_bottom = 12 end end |
#pump_one_frame ⇒ Object
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/rubycam/gtk/camera_app.rb', line 263 def pump_one_frame device.poll_frame.then do |frame| if frame @last_frame_at = monotonic_now show_frame(frame) elsif @awake && monotonic_now - (@last_frame_at || 0) > WATCHDOG_SECONDS # Video should be flowing but is not (e.g. the camera slept and woke # outside our control): rebuild the stream until frames return. @last_frame_at = monotonic_now warn 'video stalled: rebuilding stream' device.restart_streaming end end rescue SystemCallError, RuntimeError nil # stream hiccup (e.g. privacy sleep transition): keep the pump alive end |
#reconnect_camera ⇒ Object
The camera is gone (unplugged or re-enumerated): drop the dead handle and try to find it again on every poll until it returns.
195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/rubycam/gtk/camera_app.rb', line 195 def reconnect_camera panel.update(nil) close_device device.then do @failures = 0 @last_frame_at = monotonic_now end rescue SystemCallError nil end |
#reset_button ⇒ Object
115 116 117 118 119 |
# File 'lib/rubycam/gtk/camera_app.rb', line 115 def @reset_button ||= Gtk::Button.new(label: 'Reset to defaults').tap do |btn| btn.margin_top = 12 end end |
#reset_controls ⇒ Object
226 227 228 229 230 231 232 233 |
# File 'lib/rubycam/gtk/camera_app.rb', line 226 def reset_controls .each_key do |key| device.controls[key].then do |ctrl| set_control_safely(key, ctrl.default) (key).value = ctrl.default end end end |
#restore_sliders ⇒ Object
When the app started without a camera the sidebar has no sliders yet; build and attach them on first successful (re)connect.
218 219 220 221 222 223 224 |
# File 'lib/rubycam/gtk/camera_app.rb', line 218 def unless @sliders_in_sidebar || .empty? .each_value { |s| .append(s) } .reorder_child_after(, .last_child) @sliders_in_sidebar = true end end |
#set_control_safely(key, value) ⇒ Object
Control writes can fail transiently (EIO/EBUSY) while the camera enters or leaves privacy sleep; an exception here must never kill the main loop.
245 246 247 248 249 |
# File 'lib/rubycam/gtk/camera_app.rb', line 245 def set_control_safely(key, value) device.controls[key].value = value rescue SystemCallError nil end |
#show_frame(jpeg) ⇒ Object
282 283 284 285 286 287 288 289 290 |
# File 'lib/rubycam/gtk/camera_app.rb', line 282 def show_frame(jpeg) GdkPixbuf::PixbufLoader.new.tap do |loader| loader.write(jpeg) loader.close picture.pixbuf = loader.pixbuf end rescue GLib::Error nil # drop corrupt frames rather than crashing the pump end |
#sidebar ⇒ Object
99 100 101 102 103 104 105 |
# File 'lib/rubycam/gtk/camera_app.rb', line 99 def @sidebar ||= Gtk::Box.new(:vertical, 6).tap do |box| box.margin_top = 12 box.margin_bottom = 12 box.width_request = 260 end end |
#sleep_camera ⇒ Object
Sleep/wake go through OBSBOT's vendor extension unit — the same command its official software sends. This also wakes the camera after it was put to sleep by physically folding it down.
151 152 153 154 155 156 |
# File 'lib/rubycam/gtk/camera_app.rb', line 151 def sleep_camera @awake = false obsbot.sleep! rescue SystemCallError nil end |
#slider_for(ctrl) ⇒ Object
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/rubycam/gtk/camera_app.rb', line 121 def (ctrl) Gtk::Box.new(:vertical, 2).tap do |box| box.append(Gtk::Label.new(ctrl.key.to_s).tap { |l| l.halign = :start }) box.append( Gtk::Scale.new(:horizontal, Gtk::Adjustment.new(ctrl.value, ctrl.min, ctrl.max, ctrl.step, ctrl.step * 10, 0)).tap do |scale| scale.draw_value = false scale.signal_connect('value-changed') do set_control_safely(ctrl.key, scale.value.round) end end ) end end |
#slider_scale(key) ⇒ Object
235 |
# File 'lib/rubycam/gtk/camera_app.rb', line 235 def (key) = [key].last_child |
#sliders ⇒ Object
107 108 109 110 111 112 113 |
# File 'lib/rubycam/gtk/camera_app.rb', line 107 def @sliders ||= SLIDER_KEYS.filter_map do |key| device.controls[key]&.then { |ctrl| [key, (ctrl)] } end.to_h rescue SystemCallError {} end |
#start_frame_pump ⇒ Object
251 252 253 254 255 256 257 |
# File 'lib/rubycam/gtk/camera_app.rb', line 251 def start_frame_pump @last_frame_at = monotonic_now @pump = GLib::Timeout.add(FRAME_INTERVAL_MS) do pump_one_frame if @device GLib::Source::CONTINUE end end |
#start_status_sync ⇒ Object
173 174 175 176 177 178 |
# File 'lib/rubycam/gtk/camera_app.rb', line 173 def start_status_sync GLib::Timeout.add_seconds(2) do sync_status GLib::Source::CONTINUE end end |
#sync_status ⇒ Object
Poll the camera and mirror its state into the panel, so the UI follows changes made outside the app (folding the camera down, other software).
182 183 184 185 186 187 188 189 190 191 |
# File 'lib/rubycam/gtk/camera_app.rb', line 182 def sync_status obsbot.status.then do |status| @awake = !status[:asleep] @failures = 0 panel.update(status) end rescue SystemCallError @failures = (@failures || 0) + 1 reconnect_camera if @failures >= STATUS_FAILURES_BEFORE_RECONNECT end |
#toggle_compact ⇒ Object
Dashboard ⇄ compact widget mode: compact hides the preview and the V4L2 sliders, leaving just the OBSBOT panel (like Tiny4Linux's widget mode).
141 142 143 144 145 146 |
# File 'lib/rubycam/gtk/camera_app.rb', line 141 def toggle_compact @compact = !@compact picture.visible = !@compact .visible = !@compact window.set_default_size(*(@compact ? COMPACT_SIZE : FULL_SIZE)) end |
#wake_camera ⇒ Object
158 159 160 161 162 163 164 165 166 |
# File 'lib/rubycam/gtk/camera_app.rb', line 158 def wake_camera @awake = true obsbot.wake! # Grace period for the stream to resume on its own before the # watchdog rebuilds it. @last_frame_at = monotonic_now rescue SystemCallError nil end |
#window ⇒ Object
79 |
# File 'lib/rubycam/gtk/camera_app.rb', line 79 def window = @window ||= Gtk::ApplicationWindow.new(app) |