ipcam 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ipcam/main.rb ADDED
@@ -0,0 +1,344 @@
1
+ #! /usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ #
5
+ # Sample for v4l2-ruby
6
+ #
7
+ # Copyright (C) 2019 Hiroshi Kuwagata <kgt9221@gmail.com>
8
+ #
9
+
10
+ require 'v4l2'
11
+ require 'msgpack'
12
+
13
+ module IPCam
14
+ BASIS_SIZE = 640 * 480
15
+ Stop = Class.new(Exception)
16
+
17
+ class << self
18
+ def start
19
+ restore_db()
20
+
21
+ @mutex = Mutex.new
22
+ @camera = nil
23
+ @state = :READY
24
+ @img_que = Thread::Queue.new
25
+ @cam_thr = Thread.new {camera_thread}
26
+ @snd_thr = Thread.new {sender_thread}
27
+ @clients = []
28
+
29
+ WebServer.start(self)
30
+ WebSocket.start(self)
31
+
32
+ EM.run
33
+ end
34
+
35
+ def stop
36
+ @cam_thr.join
37
+
38
+ @snd_thr.raise(Stop)
39
+ @snd_thr.join
40
+
41
+
42
+ WebServer.stop
43
+ EM.stop
44
+ end
45
+
46
+ def restart_camera
47
+ @cam_thr.raise(Stop)
48
+ @cam_thr.join
49
+
50
+ @img_que.clear
51
+
52
+ @cam_thr = Thread.new {camera_thread}
53
+ end
54
+
55
+ def select_capabilities(cam)
56
+ ret = cam.frame_capabilities(:MJPEG).instance_eval {
57
+ self.sort! { |a,b|
58
+ da = (BASIS_SIZE - (a.width * a.height)).abs
59
+ db = (BASIS_SIZE - (b.width * b.height)).abs
60
+
61
+ da <=> db
62
+ }
63
+
64
+ self.first
65
+ }
66
+
67
+ return ret
68
+ end
69
+ private :select_capabilities
70
+
71
+ def pack_capability(cap)
72
+ ret = {
73
+ :width => cap.width,
74
+ :height => cap.height,
75
+ :rate => cap.rate.inject([]) { |m, n|
76
+ m << [n.numerator, n.denominator]
77
+ }
78
+ }
79
+
80
+ return ret
81
+ end
82
+ private :pack_capability
83
+
84
+ def create_capability_list(cam)
85
+ ret = cam.frame_capabilities(:MJPEG).inject([]) { |m, n|
86
+ m << pack_capability(n)
87
+ }
88
+
89
+ return ret
90
+ end
91
+ private :create_capability_list
92
+
93
+ def pack_integer_control(c)
94
+ ret = {
95
+ :type => :integer,
96
+ :id => c.id,
97
+ :name => c.name,
98
+ :value => c.default,
99
+ :min => c.min,
100
+ :max => c.max,
101
+ :step => c.step
102
+ }
103
+
104
+ return ret
105
+ end
106
+ private :pack_integer_control
107
+
108
+ def pack_boolean_control(c)
109
+ ret = {
110
+ :type => :boolean,
111
+ :id => c.id,
112
+ :name => c.name,
113
+ :value => c.default
114
+ }
115
+
116
+ return ret
117
+ end
118
+ private :pack_boolean_control
119
+
120
+ def pack_menu_control(c)
121
+ ret = {
122
+ :type => :menu,
123
+ :id => c.id,
124
+ :name => c.name,
125
+ :value => c.default,
126
+ :items => c.items.inject({}) {|m, n| m[n.name] = n.index; m}
127
+ }
128
+
129
+ return ret
130
+ end
131
+ private :pack_menu_control
132
+
133
+ def create_control_list(cam)
134
+ ret = cam.controls.inject([]) { |m, n|
135
+ case n
136
+ when Video4Linux2::Camera::IntegerControl
137
+ m << pack_integer_control(n)
138
+
139
+ when Video4Linux2::Camera::BooleanControl
140
+ m << pack_boolean_control(n)
141
+
142
+ when Video4Linux2::Camera::MenuControl
143
+ m << pack_menu_control(n)
144
+
145
+ else
146
+ raise("Unknwon control found #{n.class}")
147
+ end
148
+ }
149
+
150
+ return ret
151
+ end
152
+ private :create_control_list
153
+
154
+ def create_setting_entry(cam)
155
+ cap = select_capabilities(cam)
156
+ rate = cap.rate.sort.first
157
+
158
+ ret = {
159
+ :image_width => cap.width,
160
+ :image_height => cap.height,
161
+ :framerate => [rate.numerator, rate.denominator],
162
+ :capabilities => create_capability_list(cam),
163
+ :controls => create_control_list(cam)
164
+ }
165
+
166
+ return ret
167
+ end
168
+ private :create_setting_entry
169
+
170
+ def restore_db
171
+ begin
172
+ blob = $db_file.binread
173
+ @db = MessagePack.unpack(blob, :symbolize_keys => true)
174
+
175
+ rescue
176
+ begin
177
+ $db_file.delete
178
+ rescue Errno::ENOENT
179
+ # ignore
180
+ end
181
+
182
+ @db = {}
183
+ end
184
+ end
185
+ private :restore_db
186
+
187
+ def load_settings
188
+ ret = @db.dig(@camera.bus, @camera.name)
189
+
190
+ if not ret
191
+ ret = create_setting_entry(@camera)
192
+ (@db[@camera.bus] ||= {})[@camera.name] = ret
193
+
194
+ $db_file.binwrite(@db.to_msgpack)
195
+ end
196
+
197
+ @camera.image_height = ret[:image_height]
198
+ @camera.image_width = ret[:image_width]
199
+ @camera.framerate = Rational(*ret[:framerate])
200
+
201
+ ret[:controls].each { |ctr|
202
+ @camera.set_control(ctr[:id], ctr[:value]) rescue :ignore
203
+ }
204
+
205
+ return ret
206
+ end
207
+ private :load_settings
208
+
209
+ def broadcast(name, *args)
210
+ WebSocket.broadcast(name, *args)
211
+ end
212
+ private :broadcast
213
+
214
+ def camera_thread
215
+ $logger.info("main") {"camera thread start"}
216
+
217
+ @mutex.synchronize {
218
+ @camera = Video4Linux2::Camera.new($target)
219
+
220
+ if not @camera.support_formats.any? {|x| x.fcc == "MJPG"}
221
+ raise("#{$target} is not support Motion-JPEG")
222
+ end
223
+
224
+ @config = load_settings()
225
+
226
+ @camera.start
227
+ @state = :BUSY
228
+ }
229
+
230
+ loop {
231
+ @img_que << @camera.capture
232
+ }
233
+
234
+ rescue Stop
235
+ $logger.info("main") {"accept stop request"}
236
+ @state = :READY
237
+
238
+ rescue => e
239
+ $logger.error("main") {"camera error occured (#{e.message})"}
240
+ @state = :ABORTED
241
+
242
+ ensure
243
+ @camera&.stop if @camera&.busy?
244
+ @camera&.close
245
+ @camera = nil
246
+
247
+ $logger.info("main") {"camera thread stop"}
248
+ end
249
+ private :camera_thread
250
+
251
+ def sender_thread
252
+ $logger.info("main") {"sender thread start"}
253
+
254
+ loop {
255
+ data = @img_que.deq
256
+ @clients.each {|c| c[:que] << data}
257
+ broadcast(:update_image, {:type => "image/jpeg", :data => data})
258
+ }
259
+
260
+ rescue Stop
261
+ $logger.info("main") {"accept stop request"}
262
+ @clients.each {|c| c[:que] << nil}
263
+
264
+ ensure
265
+ $logger.info("main") {"sender thread stop"}
266
+ end
267
+
268
+ def get_camera_info
269
+ @mutex.synchronize {
270
+ ret = {
271
+ :device => $target,
272
+ :state => @state
273
+ }
274
+
275
+ ret.merge!(:bus => @camera.bus, :name => @camera.name) if @camera
276
+
277
+ return ret
278
+ }
279
+ end
280
+
281
+ def get_ident_string
282
+ raise("state violation") if @state != :BUSY
283
+ return "#{@camera.name}@#{@camera.bus}"
284
+ end
285
+
286
+ def get_config
287
+ raise("state violation") if @state != :BUSY
288
+ return @config
289
+ end
290
+
291
+ def set_image_size(width, height)
292
+ raise("state violation") if @state != :BUSY
293
+
294
+ @mutex.synchronize {
295
+ @config[:image_width] = width
296
+ @config[:image_height] = height
297
+ }
298
+
299
+ restart_camera()
300
+ broadcast(:update_image_size, width, height)
301
+ end
302
+
303
+ def set_framerate(num, deno)
304
+ raise("state violation") if @state != :BUSY
305
+
306
+ @mutex.synchronize {
307
+ @config[:framerate] = [num, deno]
308
+ }
309
+
310
+ restart_camera()
311
+ broadcast(:update_framerate, num, deno)
312
+ end
313
+
314
+ def set_control(id, val)
315
+ raise("state violation") if @state != :BUSY
316
+
317
+ entry = nil
318
+
319
+ @mutex.synchronize {
320
+ entry = @config[:controls].find {|obj| obj[:id] == id}
321
+ entry[:value] = val if entry
322
+ }
323
+
324
+ if entry
325
+ restart_camera()
326
+ broadcast(:update_control, id, val)
327
+ end
328
+ end
329
+
330
+ def save_config
331
+ @mutex.synchronize {
332
+ $db_file.binwrite(@db.to_msgpack)
333
+ }
334
+ end
335
+
336
+ def add_client(que)
337
+ @clients << {:que => que}
338
+ end
339
+
340
+ def remove_client(que)
341
+ @clients.reject! {|c| c[:que] == que}
342
+ end
343
+ end
344
+ end
@@ -0,0 +1,3 @@
1
+ module IPCam
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,178 @@
1
+ #! /usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ #
5
+ # Sample for v4l2-ruby
6
+ #
7
+ # Copyright (C) 2019 Hiroshi Kuwagata <kgt9221@gmail.com>
8
+ #
9
+
10
+ require 'sinatra/base'
11
+ require 'sinatra/streaming'
12
+ require 'puma'
13
+ require 'puma/configuration'
14
+ require 'puma/events'
15
+ require 'eventmachine'
16
+ require 'securerandom'
17
+
18
+ module IPCam
19
+ class WebServer < Sinatra::Base
20
+ set :environment, (($develop_mode)? %s{development}: %s{production})
21
+ set :views, APP_RESOURCE_DIR + "views"
22
+ set :threaded, true
23
+ set :quiet, true
24
+
25
+ enable :logging
26
+
27
+ use Rack::CommonLogger, $logger
28
+
29
+ configure :development do
30
+ before do
31
+ cache_control :no_store, :no_cache, :must_revalidate,
32
+ :max_age => 0, :post_check => 0, :pre_check => 0
33
+ headers "Pragma" => "no-cache"
34
+ end
35
+ end
36
+
37
+ helpers do
38
+ def app
39
+ return (@app ||= settings.app)
40
+ end
41
+
42
+ def find_resource(type, name)
43
+ ret = RESOURCE_DIR + "extern" + type + name
44
+ return ret if ret.exist?
45
+
46
+ ret = RESOURCE_DIR + "common" + type + name
47
+ return ret if ret.exist?
48
+
49
+ ret = APP_RESOURCE_DIR + type + name
50
+ return ret if ret.exist?
51
+
52
+ return nil
53
+ end
54
+ end
55
+
56
+ get "/" do
57
+ redirect "/main"
58
+ end
59
+
60
+ get "/main" do
61
+ erb :main
62
+ end
63
+
64
+ get "/settings" do
65
+ erb :settings
66
+ end
67
+
68
+ get "/stream" do
69
+ boundary = SecureRandom.hex(20)
70
+ queue = Thread::Queue.new
71
+
72
+ content_type("multipart/x-mixed-replace; boundary=#{boundary}")
73
+
74
+ # pumaで:keep_openが動作しないのでちょっと面倒な方法で
75
+ # 対応
76
+ stream do |port|
77
+ port.callback {
78
+ app.remove_client(queue)
79
+ queue.clear
80
+ queue << nil
81
+ }
82
+
83
+ port << "\r\n"
84
+ app.add_client(queue)
85
+
86
+ loop {
87
+ data = queue.deq
88
+ break if not data
89
+
90
+ port << <<~EOT.b
91
+ --#{boundary}
92
+ Content-Type: image/jpeg\r
93
+ Content-Length: #{data.bytesize}\r
94
+ \r
95
+ EOT
96
+
97
+ port << data
98
+ }
99
+ end
100
+ end
101
+
102
+ get %r{/css/(.+).scss} do |name|
103
+ content_type('text/css')
104
+ scss name.to_sym, :views => APP_RESOURCE_DIR + "scss"
105
+ end
106
+
107
+ get %r{/(css|js|fonts)/(.+)} do |type, name|
108
+ path = find_resource(type, name)
109
+
110
+ if path
111
+ send_file(path)
112
+ else
113
+ halt 404
114
+ end
115
+ end
116
+
117
+ class << self
118
+ def bind_url
119
+ if $bind_addr.include?(":")
120
+ addr = "[#{$bind_addr}]" if $bind_addr.include?(":")
121
+ else
122
+ addr = $bind_addr
123
+ end
124
+
125
+ return "tcp://#{addr}:#{$http_port}"
126
+ end
127
+ private :bind_url
128
+
129
+ def env_string
130
+ return ($develop_mode)? 'development':'production'
131
+ end
132
+
133
+ def start(app)
134
+ set :app, app
135
+
136
+ config = Puma::Configuration.new { |user_config|
137
+ user_config.quiet
138
+ user_config.threads(4, 4)
139
+ user_config.bind(bind_url())
140
+ user_config.environment(env_string())
141
+ user_config.force_shutdown_after(-1)
142
+ user_config.app(WebServer)
143
+ }
144
+
145
+ @events = Puma::Events.new($log_device, $log_device)
146
+ @launch = Puma::Launcher.new(config, :events => @events)
147
+
148
+ # pumaのランチャークラスでのシグナルのハンドリングが
149
+ # 邪魔なのでオーバライドして無効化する
150
+ def @launch.setup_signals
151
+ # nothing
152
+ end
153
+
154
+ @thread = Thread.start {
155
+ begin
156
+ $logger.info('webserver') {"started #{bind_url()}"}
157
+ @launch.run
158
+ ensure
159
+ $logger.info('webserver') {"stopped"}
160
+ end
161
+ }
162
+
163
+ # サーバが立ち上がりきるまで待つ
164
+ booted = false
165
+ @events.on_booted {booted = true}
166
+ sleep 0.2 until booted
167
+ end
168
+
169
+ def stop
170
+ @launch.stop
171
+ @thread.join
172
+
173
+ remove_instance_variable(:@launch)
174
+ remove_instance_variable(:@thread)
175
+ end
176
+ end
177
+ end
178
+ end