Class: Rubycam::Device

Inherits:
Object
  • Object
show all
Defined in:
lib/rubycam/device.rb

Overview

A V4L2 capture device. Controls are read/written with ioctl; frames are streamed with memory-mapped kernel buffers (uvcvideo does not support plain read()).

Constant Summary collapse

BUF_TYPE_VIDEO_CAPTURE =
1
MEMORY_MMAP =
1
CTRL_FLAG_NEXT_CTRL =
0x80000000
PROT_READ =
1
MAP_SHARED =
1
LIBC =
Fiddle.dlopen(nil)
MMAP =
Fiddle::Function.new(
  LIBC['mmap'],
  [Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_INT,
   Fiddle::TYPE_INT, Fiddle::TYPE_INT, Fiddle::TYPE_LONG],
  Fiddle::TYPE_VOIDP
)
MUNMAP =
Fiddle::Function.new(
  LIBC['munmap'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T], Fiddle::TYPE_INT
)
CAP_META_CAPTURE =
0x00800000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ Device

Returns a new instance of Device.



68
69
70
71
72
73
74
# File 'lib/rubycam/device.rb', line 68

def initialize(path)
  @path = path
  @io = File.open(path, 'r+')
  @buffers = []
  @streaming = false
  query_capabilities
end

Instance Attribute Details

#bus_infoObject (readonly)

Returns the value of attribute bus_info.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def bus_info
  @bus_info
end

#cardObject (readonly)

Returns the value of attribute card.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def card
  @card
end

#device_capsObject (readonly)

Returns the value of attribute device_caps.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def device_caps
  @device_caps
end

#driverObject (readonly)

Returns the value of attribute driver.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def driver
  @driver
end

#heightObject (readonly)

Returns the value of attribute height.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def height
  @height
end

#pathObject (readonly)

Returns the value of attribute path.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def path
  @path
end

#pixel_formatObject (readonly)

Returns the value of attribute pixel_format.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def pixel_format
  @pixel_format
end

#widthObject (readonly)

Returns the value of attribute width.



28
29
30
# File 'lib/rubycam/device.rb', line 28

def width
  @width
end

Class Method Details

.find(hint) ⇒ Object

Find a camera by device path, /dev name, or a substring of its card name or bus info — e.g. Device.find('OBSBOT Tiny 2'). Metadata-only nodes (uvcvideo exposes one per camera) are skipped. Returns nil when nothing matches.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/rubycam/device.rb', line 35

def self.find(hint)
  [hint, "/dev/#{hint}"].each do |path|
    return open(path) if File.exist?(path)
  end

  Dir['/dev/video*'].sort.each do |path|
    device = begin
      open(path)
    rescue SystemCallError
      next
    end
    if (device.card.include?(hint) || device.bus_info.include?(hint)) &&
       device.device_caps & CAP_META_CAPTURE == 0
      return device
    end
    device.close
  end
  nil
end

.open(path = '/dev/video0') ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/rubycam/device.rb', line 55

def self.open(path = '/dev/video0')
  device = new(path)
  if block_given?
    begin
      yield device
    ensure
      device.close
    end
  else
    device
  end
end

Instance Method Details

#[](key) ⇒ Object



88
# File 'lib/rubycam/device.rb', line 88

def [](key) = controls.fetch(key).value

#[]=(key, value) ⇒ Object



90
91
92
# File 'lib/rubycam/device.rb', line 90

def []=(key, value)
  controls.fetch(key).value = value
end

#capture_frame(timeout: 10.0) ⇒ Object

Block until the next frame is ready and return its bytes as a String. The default timeout is generous because the camera's ISP takes a few seconds to deliver the first frame after STREAMON.



165
166
167
168
169
170
171
172
# File 'lib/rubycam/device.rb', line 165

def capture_frame(timeout: 10.0)
  start_streaming unless @streaming
  IO.select([@io], nil, nil, timeout) or raise "timed out waiting for frame from #{path}"
  buf = dequeue_buffer
  frame = @buffers[buf[:index]][:ptr][0, buf[:bytesused]]
  queue_buffer(buf[:index])
  frame
end

#closeObject



76
77
78
79
# File 'lib/rubycam/device.rb', line 76

def close
  stop_streaming
  @io.close unless @io.closed?
end

#controlsObject

All controls the driver exposes, keyed by symbol (e.g. :zoom_absolute).



84
85
86
# File 'lib/rubycam/device.rb', line 84

def controls
  @controls ||= enumerate_controls.to_h { |c| [c.key, c] }
end

#each_frameObject



185
186
187
188
# File 'lib/rubycam/device.rb', line 185

def each_frame
  start_streaming unless @streaming
  loop { yield capture_frame }
end

#fourcc(str) ⇒ Object

---- Format / frame rate -----------------------------------------------



107
# File 'lib/rubycam/device.rb', line 107

def fourcc(str) = str.unpack1('V')

#fourcc_to_s(num) ⇒ Object



108
# File 'lib/rubycam/device.rb', line 108

def fourcc_to_s(num) = [num].pack('V')

#get_control(id) ⇒ Object



94
95
96
97
98
# File 'lib/rubycam/device.rb', line 94

def get_control(id)
  buf = [id, 0].pack('Ll')
  @io.ioctl(Ioctl::VIDIOC_G_CTRL, buf)
  buf.unpack('Ll')[1]
end

#poll_frameObject

Non-blocking: return the next frame if one is ready, else nil. Suited to GUI main loops that tick faster than the camera delivers.



176
177
178
179
180
181
182
183
# File 'lib/rubycam/device.rb', line 176

def poll_frame
  start_streaming unless @streaming
  IO.select([@io], nil, nil, 0) or return nil
  buf = dequeue_buffer
  frame = @buffers[buf[:index]][:ptr][0, buf[:bytesused]]
  queue_buffer(buf[:index])
  frame
end

#restart_streamingObject

Tear down and rebuild the buffer queue. Useful when a stream goes quiet after an external event (e.g. a camera's privacy sleep).



157
158
159
160
# File 'lib/rubycam/device.rb', line 157

def restart_streaming
  stop_streaming
  start_streaming
end

#set_control(id, value) ⇒ Object



100
101
102
103
# File 'lib/rubycam/device.rb', line 100

def set_control(id, value)
  @io.ioctl(Ioctl::VIDIOC_S_CTRL, [id, value].pack('Ll'))
  value
end

#set_format(width:, height:, pixel_format: 'MJPG') ⇒ Object

Negotiate resolution and pixel format ('MJPG' or 'YUYV'). The driver may adjust the values; the actual result lands in width/height/pixel_format.



112
113
114
115
116
117
118
119
120
121
# File 'lib/rubycam/device.rb', line 112

def set_format(width:, height:, pixel_format: 'MJPG')
  buf = [BUF_TYPE_VIDEO_CAPTURE].pack('L') + "\0" * (Ioctl::FORMAT_SIZE - 4)
  @io.ioctl(Ioctl::VIDIOC_G_FMT, buf)
  buf[8, 12] = [width, height, fourcc(pixel_format)].pack('L3')
  @io.ioctl(Ioctl::VIDIOC_S_FMT, buf)
  @width, @height, pix = buf[8, 12].unpack('L3')
  @pixel_format = fourcc_to_s(pix)
  @frame_size = buf[28, 4].unpack1('L')
  [@width, @height, @pixel_format]
end

#set_fps(fps) ⇒ Object



123
124
125
126
127
128
129
130
# File 'lib/rubycam/device.rb', line 123

def set_fps(fps)
  buf = [BUF_TYPE_VIDEO_CAPTURE].pack('L') + "\0" * (Ioctl::STREAMPARM_SIZE - 4)
  @io.ioctl(Ioctl::VIDIOC_G_PARM, buf)
  buf[12, 8] = [1, fps].pack('L2')
  @io.ioctl(Ioctl::VIDIOC_S_PARM, buf)
  num, denom = buf[12, 8].unpack('L2')
  denom / num.to_f
end

#start_streaming(buffer_count: 4) ⇒ Object

---- Streaming -----------------------------------------------------------



134
135
136
137
138
139
140
141
# File 'lib/rubycam/device.rb', line 134

def start_streaming(buffer_count: 4)
  set_format(width: 1920, height: 1080) unless @width
  request_buffers(buffer_count)
  map_buffers
  @buffers.each_index { |i| queue_buffer(i) }
  @io.ioctl(Ioctl::VIDIOC_STREAMON, [BUF_TYPE_VIDEO_CAPTURE].pack('L'))
  @streaming = true
end

#stop_streamingObject



143
144
145
146
147
148
149
150
151
# File 'lib/rubycam/device.rb', line 143

def stop_streaming
  return unless @streaming

  @io.ioctl(Ioctl::VIDIOC_STREAMOFF, [BUF_TYPE_VIDEO_CAPTURE].pack('L'))
  @buffers.each { |b| MUNMAP.call(b[:ptr], b[:length]) }
  @buffers.clear
  request_buffers(0)
  @streaming = false
end

#streaming?Boolean

Returns:

  • (Boolean)


153
# File 'lib/rubycam/device.rb', line 153

def streaming? = @streaming

#to_ioObject



190
# File 'lib/rubycam/device.rb', line 190

def to_io = @io