Class: Rubycam::Device
- Inherits:
-
Object
- Object
- Rubycam::Device
- 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
-
#bus_info ⇒ Object
readonly
Returns the value of attribute bus_info.
-
#card ⇒ Object
readonly
Returns the value of attribute card.
-
#device_caps ⇒ Object
readonly
Returns the value of attribute device_caps.
-
#driver ⇒ Object
readonly
Returns the value of attribute driver.
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#path ⇒ Object
readonly
Returns the value of attribute path.
-
#pixel_format ⇒ Object
readonly
Returns the value of attribute pixel_format.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
Class Method Summary collapse
-
.find(hint) ⇒ Object
Find a camera by device path, /dev name, or a substring of its card name or bus info — e.g.
- .open(path = '/dev/video0') ⇒ Object
Instance Method Summary collapse
- #[](key) ⇒ Object
- #[]=(key, value) ⇒ Object
-
#capture_frame(timeout: 10.0) ⇒ Object
Block until the next frame is ready and return its bytes as a String.
- #close ⇒ Object
-
#controls ⇒ Object
All controls the driver exposes, keyed by symbol (e.g. :zoom_absolute).
- #each_frame ⇒ Object
-
#fourcc(str) ⇒ Object
---- Format / frame rate -----------------------------------------------.
- #fourcc_to_s(num) ⇒ Object
- #get_control(id) ⇒ Object
-
#initialize(path) ⇒ Device
constructor
A new instance of Device.
-
#poll_frame ⇒ Object
Non-blocking: return the next frame if one is ready, else nil.
-
#restart_streaming ⇒ Object
Tear down and rebuild the buffer queue.
- #set_control(id, value) ⇒ Object
-
#set_format(width:, height:, pixel_format: 'MJPG') ⇒ Object
Negotiate resolution and pixel format ('MJPG' or 'YUYV').
- #set_fps(fps) ⇒ Object
-
#start_streaming(buffer_count: 4) ⇒ Object
---- Streaming -----------------------------------------------------------.
- #stop_streaming ⇒ Object
- #streaming? ⇒ Boolean
- #to_io ⇒ Object
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_info ⇒ Object (readonly)
Returns the value of attribute bus_info.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def bus_info @bus_info end |
#card ⇒ Object (readonly)
Returns the value of attribute card.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def card @card end |
#device_caps ⇒ Object (readonly)
Returns the value of attribute device_caps.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def device_caps @device_caps end |
#driver ⇒ Object (readonly)
Returns the value of attribute driver.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def driver @driver end |
#height ⇒ Object (readonly)
Returns the value of attribute height.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def height @height end |
#path ⇒ Object (readonly)
Returns the value of attribute path.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def path @path end |
#pixel_format ⇒ Object (readonly)
Returns the value of attribute pixel_format.
28 29 30 |
# File 'lib/rubycam/device.rb', line 28 def pixel_format @pixel_format end |
#width ⇒ Object (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 |
#close ⇒ Object
76 77 78 79 |
# File 'lib/rubycam/device.rb', line 76 def close stop_streaming @io.close unless @io.closed? end |
#controls ⇒ Object
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_frame ⇒ Object
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_frame ⇒ Object
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_streaming ⇒ Object
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_streaming ⇒ Object
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
153 |
# File 'lib/rubycam/device.rb', line 153 def streaming? = @streaming |
#to_io ⇒ Object
190 |
# File 'lib/rubycam/device.rb', line 190 def to_io = @io |