ipcam 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,380 @@
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 'fileutils'
11
+ require 'em-websocket'
12
+ require 'msgpack/rpc/server'
13
+
14
+ module IPCam
15
+ class WebSocket
16
+ include MessagePack::Rpc::Server
17
+
18
+ msgpack_options :symbolize_keys => true
19
+
20
+ class << self
21
+ #
22
+ # セッションリストの取得
23
+ #
24
+ # @return [Array<WebSocket>] セッションリスト
25
+ #
26
+ def session_list
27
+ return @session_list ||= []
28
+ end
29
+ private :session_list
30
+
31
+ #
32
+ # クリティカルセクションの設置
33
+ #
34
+ # @yield クリティカルセクションとして処理するブロック
35
+ #
36
+ # @return [Object] ブロックの戻り値
37
+ #
38
+ def sync(&proc)
39
+ return (@mutex ||= Mutex.new).synchronize(&proc)
40
+ end
41
+
42
+ #
43
+ # セッションリストへのセッション追加
44
+ #
45
+ # @param [Socket] sock セッションリストに追加するソケットオブジェクト
46
+ #
47
+ # @return [WebSocket]
48
+ # ソケットオブジェクトに紐付けられたセッションオブジェクト
49
+ #
50
+ # @note
51
+ # 受け取ったソケットオブジェクトを元に、セッションオブジェクトを
52
+ # 生成し、そのオブジェクトをセッションリストに追加する
53
+ #
54
+ def join(sock)
55
+ sync {
56
+ if session_list.any? {|s| s === sock}
57
+ raise("Already joined #{sock}")
58
+ end
59
+
60
+ ret = self.new(@app, sock)
61
+ session_list << ret
62
+
63
+ return ret
64
+ }
65
+ end
66
+ private :join
67
+
68
+ #
69
+ # セッションオブジェクトからのセッション削除
70
+ #
71
+ # @param [Socket] sock
72
+ # セッションオブジェクトを特定するためのソケットオブジェクト
73
+ #
74
+ def bye(sock)
75
+ sync {
76
+ session_list.reject! { |s|
77
+ if s === sock
78
+ s.finish
79
+ true
80
+ else
81
+ false
82
+ end
83
+ }
84
+ }
85
+ end
86
+
87
+ #
88
+ # イベント情報の一斉送信
89
+ #
90
+ # @param [String] name イベント名
91
+ # @param [Array] args イベントで通知する引数
92
+ #
93
+ def broadcast(name, *args)
94
+ sync {session_list.each {|s| s.notify(name, *args)}}
95
+ end
96
+
97
+ #
98
+ # バインド先のURL文字列を生成する
99
+ #
100
+ # @return [String] URL文字列
101
+ #
102
+ def bind_url
103
+ if $bind_addr.include?(":")
104
+ addr = "[#{$bind_addr}]" if $bind_addr.include?(":")
105
+ else
106
+ addr = $bind_addr
107
+ end
108
+
109
+ return "tcp://#{addr}:#{$http_port}"
110
+ end
111
+ private :bind_url
112
+
113
+ #
114
+ # WebSocket制御の開始
115
+ #
116
+ def start(app)
117
+ EM.defer {
118
+ @app = app
119
+
120
+ sleep 1 until EM.reactor_running?
121
+
122
+ $logger.info("websock") {"started (#{bind_url()})"}
123
+
124
+ EM::WebSocket.start(:host => $bind_addr, :port => $ws_port) { |sock|
125
+ peer = Socket.unpack_sockaddr_in(sock.get_peername)
126
+ addr = peer[1]
127
+ port = peer[0]
128
+ serv = join(sock)
129
+
130
+ sock.set_sock_opt(Socket::Constants::SOL_SOCKET,
131
+ Socket::SO_KEEPALIVE,
132
+ true)
133
+
134
+ sock.set_sock_opt(Socket::IPPROTO_TCP,
135
+ Socket::TCP_QUICKACK,
136
+ true)
137
+
138
+ sock.set_sock_opt(Socket::IPPROTO_TCP,
139
+ Socket::TCP_NODELAY,
140
+ false)
141
+
142
+ sock.onopen {
143
+ $logger.info("websock") {"connection from #{addr}:#{port}"}
144
+ }
145
+
146
+ sock.onbinary { |msg|
147
+ begin
148
+ serv.receive_dgram(msg)
149
+
150
+ rescue => e
151
+ $logger.error("websock") {
152
+ "error occured: #{e.message} (#{e.backtrace[0]})"
153
+ }
154
+ end
155
+ }
156
+
157
+ sock.onclose {
158
+ $logger.info("websock") {
159
+ "connection close from #{addr}:#{port}"
160
+ }
161
+
162
+ bye(sock)
163
+ }
164
+ }
165
+ }
166
+ end
167
+ end
168
+
169
+ #
170
+ # セッションオブジェクトのイニシャライザ
171
+ #
172
+ # @param [IPCam] app アプリケーション本体のインスタンス
173
+ # @param [Socket] sock Socketインスタンス
174
+ #
175
+ def initialize(app, sock)
176
+ @app = app
177
+ @sock = sock
178
+ @allow = []
179
+
180
+ peer = Socket.unpack_sockaddr_in(sock.get_peername)
181
+ @addr = peer[1]
182
+ @port = peer[0]
183
+ end
184
+
185
+ attr_reader :sock
186
+
187
+ #
188
+ # セッションオブジェクトの終了処理
189
+ #
190
+ def finish
191
+ end
192
+
193
+ #
194
+ # peerソケットへのデータ送信
195
+ #
196
+ # @param [String] data 送信するデータ
197
+ #
198
+ # @note MessagePack::Rpc::Serverのオーバーライド
199
+ #
200
+ def send_data(data)
201
+ @sock.send_binary(data)
202
+ end
203
+ private :send_data
204
+
205
+ #
206
+ # MessagePack-RPCのエラーハンドリング
207
+ #
208
+ # @param [StandardError] e 発生したエラーの例外オブジェクト
209
+ #
210
+ # @note MessagePack::Rpc::Serverのオーバーライド
211
+ #
212
+ def on_error(e)
213
+ $logger.error("websock") {e.message}
214
+ end
215
+ private :on_error
216
+
217
+ #
218
+ # 通知のブロードキャスト
219
+ #
220
+ # @param [String] name イベント名
221
+ # @param [Array] args イベントで通知する引数
222
+ #
223
+ def broadcast(name, *args)
224
+ self.class.broadcast(name, *arg)
225
+ end
226
+ private :broadcast
227
+
228
+ #
229
+ # 通知の送信
230
+ #
231
+ # @param [String] name イベント名
232
+ # @param [Array] args イベントで通知する引数
233
+ #
234
+ def notify(name, *args)
235
+ super(name, *args) if @allow == "*" or @allow.include?(name)
236
+ end
237
+
238
+ #
239
+ # 比較演算子の定義
240
+ #
241
+ def ===(obj)
242
+ return (self == obj || @sock == obj)
243
+ end
244
+
245
+ #
246
+ # RPC procedures
247
+ #
248
+
249
+ #
250
+ # 通知要求を設定する
251
+ #
252
+ # @param [Array] arg
253
+ #
254
+ # @return [:OK] 固定値
255
+ #
256
+ def add_notify_request(*args)
257
+ args.each {|type| @allow << type.to_sym}
258
+ args.uniq!
259
+
260
+ return :OK
261
+ end
262
+ remote_public :add_notify_request
263
+
264
+ #
265
+ # 通知要求をクリアする
266
+ #
267
+ # @param [Array] arg
268
+ #
269
+ # @return [:OK] 固定値
270
+ #
271
+ def clear_notify_request(*args)
272
+ args.each {|type| @allow.delete(type.to_sym)}
273
+
274
+ return :OK
275
+ end
276
+ remote_public :clear_notify_request
277
+
278
+ #
279
+ # 疎通確認用プロシジャー
280
+ #
281
+ # @return [:OK] 固定値
282
+ #
283
+ def hello
284
+ return :OK
285
+ end
286
+ remote_public :hello
287
+
288
+ #
289
+ # カメラ情報の取得
290
+ #
291
+ # @return [:OK] カメラ情報をパックしたハッシュ
292
+ #
293
+ def get_camera_info
294
+ return @app.get_camera_info()
295
+ end
296
+ remote_public :get_camera_info
297
+
298
+ #
299
+ # カメラ固有名の取得
300
+ #
301
+ # @return [:OK] カメラ情報をパックしたハッシュ
302
+ #
303
+ def get_ident_string
304
+ return @app.get_ident_string()
305
+ end
306
+ remote_public :get_ident_string
307
+
308
+ #
309
+ # カメラの設定情報の取得
310
+ #
311
+ # @return [Array] カメラの設定情報の配列
312
+ #
313
+ def get_config
314
+ return @app.get_config
315
+ end
316
+ remote_public :get_config
317
+
318
+ #
319
+ # 画像サイズの設定
320
+ #
321
+ # @param [Integer] width 新しい画像の幅
322
+ # @param [Integer] height 新しい画像の高さ
323
+ #
324
+ # @return [:OK] 固定値
325
+ #
326
+ # @note 画像サイズの変更に伴い、update_image_sizeイベントがブロード
327
+ # キャストされる
328
+ #
329
+ def set_image_size(width, height)
330
+ @app.set_image_size(width, height)
331
+ return :OK
332
+ end
333
+ remote_public :set_image_size
334
+
335
+ #
336
+ # フレームレートの設定
337
+ #
338
+ # @param [Integer] num 新しいフレームレートの値(分子)
339
+ # @param [Integer] deno 新しいフレームレートの値(分母)
340
+ #
341
+ # @return [:OK] 固定値
342
+ #
343
+ # @note 画像サイズの変更に伴い、update_framerateイベントがブロード
344
+ # キャストされる
345
+ #
346
+ def set_framerate(num, deno)
347
+ @app.set_framerate(num, deno)
348
+ return :OK
349
+ end
350
+ remote_public :set_framerate
351
+
352
+ #
353
+ # カメラの設定変更
354
+ #
355
+ # @param [Integer] id 設定項目のID
356
+ # @param [Integer] val 新しい設定項目の値
357
+ #
358
+ # @return [:OK] 固定値
359
+ #
360
+ # @note 画像サイズの変更に伴い、update_controlイベントがブロード
361
+ # キャストされる
362
+ #
363
+ def set_control(num, deno)
364
+ @app.set_control(num, deno)
365
+ return :OK
366
+ end
367
+ remote_public :set_control
368
+
369
+ #
370
+ # 設定値の保存
371
+ #
372
+ # @return [:OK] 固定値
373
+ #
374
+ def save_config
375
+ @app.save_config()
376
+ return :OK
377
+ end
378
+ remote_public :save_config
379
+ end
380
+ end
@@ -0,0 +1,191 @@
1
+ /*
2
+ * MessagePack RPC base class
3
+ *
4
+ * (C) 2017 Hiroshi Kuwagata <kgt9221@gmail.com>
5
+ */
6
+
7
+ if (!msgpack) {
8
+ throw "msgpack-lite is not load yet"
9
+ }
10
+
11
+ (function () {
12
+ /* declar local symbol */
13
+ const newId = Symbol('newId');
14
+ const callback = Symbol('callback');
15
+ const recv = Symbol('recv');
16
+
17
+ /* declar contant */
18
+ const DEFAULT_URL = `ws://${location.hostname}:${parseInt(location.port)+1}/`;
19
+ const COEDEC = msgpack.createCodec({binarraybuffer:true, preset:true});
20
+ const ENC_OPT = {codec:COEDEC};
21
+
22
+ msgpack.rpc = class {
23
+ /*
24
+ * core functions
25
+ */
26
+ constructor (url) {
27
+ this.url = url || DEFAULT_URL
28
+ this.sock = null;
29
+ this.deferred = null;
30
+ this.maxId = 1;
31
+ this.handlers = {}
32
+ }
33
+
34
+ [newId]() {
35
+ return this.maxId++;
36
+ }
37
+
38
+ [callback](name, args) {
39
+ if (this.handlers[name]) {
40
+ this.handlers[name](...args);
41
+ } else {
42
+ throw `unhandled notification received (${name}).`;
43
+ }
44
+ }
45
+
46
+ [recv](data) {
47
+ var self;
48
+ var msg;
49
+ var type;
50
+ var id;
51
+ var meth;
52
+ var err;
53
+ var res;
54
+ var para;
55
+ var $df;
56
+
57
+ msg = msgpack.decode(new Uint8Array(data), ENC_OPT);
58
+ type = msg[0];
59
+
60
+ switch (type) {
61
+ case 1:
62
+ id = msg[1];
63
+ err = msg[2];
64
+ res = msg[3];
65
+ $df = this.deferred[id];
66
+
67
+ if ($df) {
68
+ if (err) {
69
+ $df.reject(err);
70
+ } else {
71
+ $df.resolve(res);
72
+ }
73
+ }
74
+
75
+ delete this.deferred[id];
76
+ break;
77
+
78
+ case 2:
79
+ meth = msg[1];
80
+ para = msg[2];
81
+
82
+ if (!para) {
83
+ para = [];
84
+ } else if (!(para instanceof Array)) {
85
+ para = [para];
86
+ }
87
+
88
+ this[callback](meth, para);
89
+ break;
90
+
91
+ default:
92
+ throw `Illeagal data (type=${type}) recevied.`;
93
+ }
94
+ }
95
+
96
+ remoteCall(meth) {
97
+ var id;
98
+ var $df;
99
+ var args;
100
+
101
+ id = this[newId]();
102
+ $df = new $.Deferred();
103
+ args = Array.prototype.slice.call(arguments, 1);
104
+
105
+ switch (args.length) {
106
+ case 0:
107
+ args = null;
108
+ break;
109
+
110
+ case 1:
111
+ args = args[0];
112
+ break;
113
+ }
114
+
115
+ this.deferred[id] = $df;
116
+ this.sock.send(msgpack.encode([0, id, meth, args], ENC_OPT));
117
+
118
+ return $df.promise();
119
+ }
120
+
121
+ remoteNotify(meth) {
122
+ var args;
123
+
124
+ args = Array.prototype.slice.call(arguments, 1);
125
+
126
+ switch (args.length) {
127
+ case 0:
128
+ args = null;
129
+ break;
130
+
131
+ case 1:
132
+ args = args[0];
133
+ break;
134
+ }
135
+
136
+ this.sock.send(msgpack.encode([2, meth, args], ENC_OPT));
137
+ }
138
+
139
+ on(name, func) {
140
+ this.handlers[name] = func;
141
+ return this;
142
+ }
143
+
144
+ start() {
145
+ var $df;
146
+
147
+ $df = new $.Deferred();
148
+
149
+ if (!this.sock) {
150
+ this.sock = new WebSocket(this.url);
151
+
152
+ this.sock.binaryType = "arraybuffer";
153
+
154
+ this.sock.onopen = () => {
155
+ this.deferred = {}
156
+ $df.resolve();
157
+ };
158
+
159
+ this.sock.onerror = () => {
160
+ $df.reject();
161
+ };
162
+
163
+ this.sock.onmessage = (m) => {
164
+ this[recv](m.data);
165
+ };
166
+
167
+ this.sock.onclose = () => {
168
+ if (this.onSessionClosed) this.onSessionClosed();
169
+ this.sock = null;
170
+ };
171
+ }
172
+
173
+ return $df.promise();
174
+ }
175
+
176
+ finish() {
177
+ var id;
178
+
179
+ this.sock.close();
180
+
181
+ for (id in this.deferred) {
182
+ this.deferred[id].reject("session finished");
183
+ delete this.deferred[id];
184
+ }
185
+
186
+ this.sock = null;
187
+ this.deferred = null;
188
+ }
189
+ }
190
+ })();
191
+
@@ -0,0 +1,135 @@
1
+ /*
2
+ * 雑多な処理を集めたコード
3
+ *
4
+ * (C) 2017 Hiroshi Kuwagata <kgt9221@gmail.com>
5
+ */
6
+
7
+ (function () {
8
+ function readJavaScript(src) {
9
+ return $.getScript(src);
10
+ }
11
+
12
+ function readCss(src) {
13
+ var $df;
14
+
15
+ $df = new $.Deferred();
16
+
17
+ $('head')
18
+ .append($('<link>')
19
+ .on('load', () => {
20
+ $df.resolve();
21
+ })
22
+ .attr("rel", "stylesheet")
23
+ .attr("type", "text/css")
24
+ .attr("href", src)
25
+ );
26
+
27
+ return $df.promise();
28
+ }
29
+
30
+ function getResource(list, $df) {
31
+ var src;
32
+
33
+ src = list.shift();
34
+
35
+ if (/\.js$/.test(src)) {
36
+ readJavaScript(src)
37
+ .then((script, state) => {
38
+ getResource(list, $df);
39
+ })
40
+ .fail((error) => {
41
+ $df.reject(error);
42
+ });
43
+
44
+ } else if (/\.(css|scss)$/.test(src)) {
45
+ readCss(src)
46
+ .then(() => {
47
+ getResource(list, $df);
48
+ })
49
+ .fail((error) => {
50
+ $df.reject(error);
51
+ });
52
+
53
+ } else if (src == null) {
54
+ $df.resolve();
55
+ }
56
+ }
57
+
58
+ function loadImage(url, dst) {
59
+ var $df;
60
+
61
+ $df = new $.Deferred();
62
+
63
+ if (!dst) {
64
+ dst = new Image();
65
+ } else {
66
+ if (dst instanceof jQuery) {
67
+ dst = dst[0];
68
+ }
69
+
70
+ if (!(dst instanceof Image)) {
71
+ throw("not image object");
72
+ }
73
+ }
74
+
75
+ $(dst)
76
+ .on('load', () => {
77
+ $df.resolve(dst);
78
+ })
79
+ .on('error', (e) => {
80
+ $df.reject(e);
81
+ })
82
+ .attr('src', url);
83
+
84
+ return $df.promise();
85
+ }
86
+
87
+ Utils = class {
88
+ static require(list) {
89
+ var $df;
90
+
91
+ $df = new $.Deferred()
92
+
93
+ getResource(list, $df);
94
+
95
+ return $df.promise();
96
+ }
97
+
98
+ static loadImageFromData(data) {
99
+ var $df;
100
+ var blob;
101
+ var url;
102
+
103
+ $df = new $.Deferred();
104
+ blob = new Blob([data.data], {type: data.type});
105
+ url = URL.createObjectURL(blob);
106
+
107
+ loadImage(url)
108
+ .then((img) => {
109
+ $df.resolve(img);
110
+ })
111
+ .fail((error) => {
112
+ $df.reject(error);
113
+ })
114
+ .always(() => {
115
+ URL.revokeObjectURL(url);
116
+ });
117
+
118
+ return $df.promise();
119
+ }
120
+
121
+ static copyToClipboard(text) {
122
+ var $text;
123
+
124
+ $text = $('<textarea>').css('visible', 'hidden');
125
+ $('body').append($text);
126
+
127
+ $text
128
+ .val(text)
129
+ .select();
130
+ document.execCommand('copy');
131
+
132
+ $text.remove();
133
+ }
134
+ }
135
+ })();