Class: Rubycam::GTK::Video4Linux

Inherits:
Object
  • Object
show all
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

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

#appObject



77
# File 'lib/rubycam/gtk/video4linux.rb', line 77

def app = @app ||= Gtk::Application.new('org.rubycam.v4l2', :default_flags)

#buildObject



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(sidebar)

        sidebar.tap do |side|
          sliders.each_value { |s| side.append(s) }
          side.append(reset_button)
          @sliders_in_sidebar = !sliders.empty?

          reset_button.tap do |btn|
            btn.signal_connect('clicked') { reset_controls }
          end
        end
      end

      start_frame_pump
      start_reconnect_watch
      window.present
    end
  end
end

#close_deviceObject



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

#deviceObject

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

#layoutObject



79
# File 'lib/rubycam/gtk/video4linux.rb', line 79

def layout = @layout ||= Gtk::Box.new(:horizontal, 12)

#monotonic_nowObject



225
# File 'lib/rubycam/gtk/video4linux.rb', line 225

def monotonic_now = Process.clock_gettime(Process::CLOCK_MONOTONIC)

#pictureObject



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.hexpand = true
    pic.vexpand = true
    pic.margin_start = 12
    pic.margin_top = 12
    pic.margin_bottom = 12
  end
end

#pump_one_frameObject



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_cameraObject



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
    restore_sliders
    window.title = window_title
  end
rescue SystemCallError
  nil
end

#reset_buttonObject



108
109
110
111
112
# File 'lib/rubycam/gtk/video4linux.rb', line 108

def reset_button
  @reset_button ||= Gtk::Button.new(label: 'Reset to defaults').tap do |btn|
    btn.margin_top = 12
  end
end

#reset_controlsObject



172
173
174
175
176
177
178
179
# File 'lib/rubycam/gtk/video4linux.rb', line 172

def reset_controls
  sliders.each_key do |key|
    device.controls[key].then do |ctrl|
      set_control_safely(key, ctrl.default)
      slider_scale(key).value = ctrl.default
    end
  end
end

#restore_slidersObject

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 restore_sliders
  unless @sliders_in_sidebar || sliders.empty?
    sliders.each_value { |s| sidebar.append(s) }
    sidebar.reorder_child_after(reset_button, sidebar.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


91
92
93
94
95
96
97
98
# File 'lib/rubycam/gtk/video4linux.rb', line 91

def sidebar
  @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 slider_for(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 slider_scale(key) = sliders[key].last_child

#slidersObject



100
101
102
103
104
105
106
# File 'lib/rubycam/gtk/video4linux.rb', line 100

def sliders
  @sliders ||= SLIDER_KEYS.filter_map do |key|
    device.controls[key]&.then { |ctrl| [key, slider_for(ctrl)] }
  end.to_h
rescue SystemCallError
  {}
end

#start_frame_pumpObject



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_watchObject

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

#windowObject



78
# File 'lib/rubycam/gtk/video4linux.rb', line 78

def window = @window ||= Gtk::ApplicationWindow.new(app)

#window_titleObject



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