Class: Rubycam::GTK::Video4Linux
- Inherits:
-
Object
- Object
- Rubycam::GTK::Video4Linux
- Defined in:
- lib/rubycam/gtk/video4linux.rb
Constant Summary collapse
- FRAME_INTERVAL_MS =
16- SLIDER_KEYS =
%i[pan_absolute tilt_absolute zoom_absolute brightness contrast saturation sharpness].freeze
- WINDOW_SIZE =
[1120, 640].freeze
- STREAM_ERRORS_BEFORE_DROP =
After this many consecutive frame-pump errors the camera is treated as gone (a single hiccup while a stream rebuilds must not drop the handle).
60- 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
-
#device ⇒ Object
Accepts a device path, a /dev name, or a card/bus substring (e.g. 'Integrated Camera').
-
#initialize(device_path) ⇒ Video4Linux
constructor
A new instance of Video4Linux.
- #layout ⇒ Object
- #monotonic_now ⇒ Object
- #picture ⇒ Object
- #pump_one_frame ⇒ Object
- #reconnect_camera ⇒ Object
- #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); an exception here must never kill the main loop.
- #show_frame(jpeg) ⇒ Object
- #sidebar ⇒ Object
- #slider_for(ctrl) ⇒ Object
- #slider_scale(key) ⇒ Object
- #sliders ⇒ Object
- #start_frame_pump ⇒ Object
-
#start_reconnect_watch ⇒ Object
Drop the dead handle and try to find the camera again on every tick until it returns (unplugged or re-enumerated).
- #window ⇒ Object
- #window_title ⇒ Object
Constructor Details
#initialize(device_path) ⇒ Video4Linux
Returns a new instance of Video4Linux.
18 19 20 |
# File 'lib/rubycam/gtk/video4linux.rb', line 18 def initialize(device_path) @device_path = device_path end |
Instance Method Details
#app ⇒ Object
77 |
# File 'lib/rubycam/gtk/video4linux.rb', line 77 def app = @app ||= Gtk::Application.new('org.rubycam.v4l2', :default_flags) |
#build ⇒ Object
22 23 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 |
# File 'lib/rubycam/gtk/video4linux.rb', line 22 def build app.tap do app.signal_connect('activate') do app.add_window(window) window.tap do |win| win.title = window_title win.set_default_size(*WINDOW_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() .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_reconnect_watch window.present end end end |
#close_device ⇒ Object
154 155 156 157 158 159 160 |
# File 'lib/rubycam/gtk/video4linux.rb', line 154 def close_device @device&.close rescue SystemCallError nil ensure @device = nil end |
#device ⇒ Object
Accepts a device path, a /dev name, or a card/bus substring (e.g. 'Integrated Camera').
63 64 65 66 67 68 69 |
# File 'lib/rubycam/gtk/video4linux.rb', line 63 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
79 |
# File 'lib/rubycam/gtk/video4linux.rb', line 79 def layout = @layout ||= Gtk::Box.new(:horizontal, 12) |
#monotonic_now ⇒ Object
225 |
# File 'lib/rubycam/gtk/video4linux.rb', line 225 def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC) |
#picture ⇒ Object
81 82 83 84 85 86 87 88 89 |
# File 'lib/rubycam/gtk/video4linux.rb', line 81 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
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/rubycam/gtk/video4linux.rb', line 204 def pump_one_frame device.poll_frame.then do |frame| if frame @last_frame_at = monotonic_now @stream_errors = 0 show_frame(frame) elsif monotonic_now - (@last_frame_at || 0) > WATCHDOG_SECONDS # Video should be flowing but is not: rebuild the stream until frames # return. @last_frame_at = monotonic_now warn 'video stalled: rebuilding stream' device.restart_streaming end end rescue SystemCallError, RuntimeError # Sustained errors mean the camera is gone: drop the handle so the # reconnect watch re-finds it. A one-off hiccup is ignored. @stream_errors = (@stream_errors || 0) + 1 close_device if @stream_errors >= STREAM_ERRORS_BEFORE_DROP end |
#reconnect_camera ⇒ Object
143 144 145 146 147 148 149 150 151 152 |
# File 'lib/rubycam/gtk/video4linux.rb', line 143 def reconnect_camera device.then do @stream_errors = 0 @last_frame_at = monotonic_now window.title = window_title end rescue SystemCallError nil end |
#reset_button ⇒ Object
108 109 110 111 112 |
# File 'lib/rubycam/gtk/video4linux.rb', line 108 def @reset_button ||= Gtk::Button.new(label: 'Reset to defaults').tap do |btn| btn.margin_top = 12 end end |
#reset_controls ⇒ Object
172 173 174 175 176 177 178 179 |
# File 'lib/rubycam/gtk/video4linux.rb', line 172 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.
164 165 166 167 168 169 170 |
# File 'lib/rubycam/gtk/video4linux.rb', line 164 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); an exception here must never kill the main loop.
185 186 187 188 189 |
# File 'lib/rubycam/gtk/video4linux.rb', line 185 def set_control_safely(key, value) device.controls[key].value = value rescue SystemCallError nil end |
#show_frame(jpeg) ⇒ Object
227 228 229 230 231 232 233 234 235 |
# File 'lib/rubycam/gtk/video4linux.rb', line 227 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
91 92 93 94 95 96 97 98 |
# File 'lib/rubycam/gtk/video4linux.rb', line 91 def @sidebar ||= Gtk::Box.new(:vertical, 6).tap do |box| box.margin_top = 12 box.margin_bottom = 12 box.margin_end = 12 box.width_request = 260 end end |
#slider_for(ctrl) ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/rubycam/gtk/video4linux.rb', line 114 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
181 |
# File 'lib/rubycam/gtk/video4linux.rb', line 181 def (key) = [key].last_child |
#sliders ⇒ Object
100 101 102 103 104 105 106 |
# File 'lib/rubycam/gtk/video4linux.rb', line 100 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
191 192 193 194 195 196 197 198 |
# File 'lib/rubycam/gtk/video4linux.rb', line 191 def start_frame_pump @last_frame_at = monotonic_now @stream_errors = 0 @pump = GLib::Timeout.add(FRAME_INTERVAL_MS) do pump_one_frame if @device GLib::Source::CONTINUE end end |
#start_reconnect_watch ⇒ Object
Drop the dead handle and try to find the camera again on every tick until it returns (unplugged or re-enumerated).
136 137 138 139 140 141 |
# File 'lib/rubycam/gtk/video4linux.rb', line 136 def start_reconnect_watch GLib::Timeout.add_seconds(2) do reconnect_camera unless @device GLib::Source::CONTINUE end end |
#window ⇒ Object
78 |
# File 'lib/rubycam/gtk/video4linux.rb', line 78 def window = @window ||= Gtk::ApplicationWindow.new(app) |
#window_title ⇒ Object
71 72 73 74 75 |
# File 'lib/rubycam/gtk/video4linux.rb', line 71 def window_title device.card rescue SystemCallError 'Rubycam V4L2 (no camera)' end |