ipcam 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93b61fe6e5f73409e2849fef325d17ab2346a1976b42bcc26b8c6101740d4966
4
- data.tar.gz: fc43d179a3351208cda3e647dd1f36851a7a587c253e560f1f80f1d813802478
3
+ metadata.gz: 113a2a9bb9fd4e1a578898654c0a82926abc332d02aea58567e9530ed59b7c37
4
+ data.tar.gz: fbd86fd23d34fedc6e491bed2d823e03d8f5cc5afd4993a85d1ff3613a203f7f
5
5
  SHA512:
6
- metadata.gz: d6c0b022dc7022de123b67a291e2f455379e1c3abc322ff4d5752951aa6b024b2f6d8e8d7dca4de58dc40dee6833fc65ffcde79a4063c797e19ae5f9daafba62
7
- data.tar.gz: 55533db3f31edd4baa3a7cdd6030acabf7b3d04f3abe1c1bb95c90a3a9754485f5be2c5349d676df59cc30c6838af987b171eab584e4804c4c8df78da247fe8b
6
+ metadata.gz: 65c5542f695a62cc5f818cc95cf554763eda0d334fea9c36181587fd4e298f49695e205fca21925a92006cba966a2c8e49c569e5e842bd0887f1c3c21aeff7e9
7
+ data.tar.gz: 4775df55c9a5a3533df89da431b724d7fddd4c86c2f0c259aa15a46ec833d95c5bac960a1959e012f0eafa31650dbc698620b5a7e614f4094b6280748ab44b97
data/README.md CHANGED
@@ -18,6 +18,7 @@ Or install it yourself as:
18
18
  $ gem install ipcam
19
19
 
20
20
  ## Usage
21
+ Connect a camera device compatible with V4L2 and start ipcam as follows.
21
22
  ```
22
23
  ipcam [options] [device-file]
23
24
  options:
@@ -30,6 +31,11 @@ options:
30
31
  --log-level=LEVEL
31
32
  --develop-mode
32
33
  ```
34
+ Then connect to port 4567 by http browser and operate. The accessible URLs are as follows.
35
+
36
+ * http://${HOST}:4567/<br>redicrect to /main
37
+ * http://${HOST}:4567/main<br>preview and settings
38
+ * http://${HOST}:4567/stream<br>http streaming
33
39
 
34
40
  ### options
35
41
  <dl>
@@ -55,5 +61,9 @@ options:
55
61
  ### device-file
56
62
  specify target device file (ex: /dev/video1). if omittedm, it will use "/dev/video0".
57
63
 
64
+ ## etc
65
+ ### About image data
66
+ いらすとや(https://www.irasutoya.com/)で配布されている『特撮映画のイラスト』(https://www.irasutoya.com/2018/12/blog-post_90.html)を改変して使用しています。
67
+
58
68
  ## License
59
69
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/bin/ipcam CHANGED
@@ -91,7 +91,6 @@ OptionParser.new { |opt|
91
91
 
92
92
  opt.order!(ARGV)
93
93
 
94
-
95
94
  if log[:device]
96
95
  $log_device = File.open(log[:device], "a")
97
96
  $log_device.sync = true
data/lib/ipcam/main.rb CHANGED
@@ -13,6 +13,7 @@ require 'msgpack'
13
13
  module IPCam
14
14
  BASIS_SIZE = 640 * 480
15
15
  Stop = Class.new(Exception)
16
+ Restart = Class.new(Exception)
16
17
 
17
18
  class << self
18
19
  def start
@@ -20,7 +21,7 @@ module IPCam
20
21
 
21
22
  @mutex = Mutex.new
22
23
  @camera = nil
23
- @state = :READY
24
+ @state = :STOP
24
25
  @img_que = Thread::Queue.new
25
26
  @cam_thr = Thread.new {camera_thread}
26
27
  @snd_thr = Thread.new {sender_thread}
@@ -44,12 +45,7 @@ module IPCam
44
45
  end
45
46
 
46
47
  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}
48
+ @cam_thr.raise(Restart)
53
49
  end
54
50
 
55
51
  def select_capabilities(cam)
@@ -169,8 +165,16 @@ module IPCam
169
165
 
170
166
  def restore_db
171
167
  begin
172
- blob = $db_file.binread
173
- @db = MessagePack.unpack(blob, :symbolize_keys => true)
168
+ blob = $db_file.binread
169
+ @db = MessagePack.unpack(blob, :symbolize_keys => true)
170
+
171
+ @db.keys { |bus|
172
+ @db[bus].keys { |name|
173
+ @db[bus][name.to_s] = @db[bus].delete(name)
174
+ }
175
+
176
+ @db[bus.to_s] = @db.deleye(bus)
177
+ }
174
178
 
175
179
  rescue
176
180
  begin
@@ -179,13 +183,13 @@ module IPCam
179
183
  # ignore
180
184
  end
181
185
 
182
- @db = {}
186
+ @db = {}
183
187
  end
184
188
  end
185
189
  private :restore_db
186
190
 
187
191
  def load_settings
188
- ret = @db.dig(@camera.bus, @camera.name)
192
+ ret = @db.dig(@camera.bus.to_sym, @camera.name.to_sym)
189
193
 
190
194
  if not ret
191
195
  ret = create_setting_entry(@camera)
@@ -211,39 +215,62 @@ module IPCam
211
215
  end
212
216
  private :broadcast
213
217
 
218
+ def change_state(state)
219
+ flag = @mutex.try_lock
220
+
221
+ if @state != state
222
+ @state = state
223
+ broadcast(:change_state, state)
224
+ end
225
+
226
+ @mutex.unlock if flag
227
+ end
228
+ private :change_state
229
+
214
230
  def camera_thread
215
231
  $logger.info("main") {"camera thread start"}
216
232
 
217
- @mutex.synchronize {
218
- @camera = Video4Linux2::Camera.new($target)
233
+ @camera = Video4Linux2::Camera.new($target)
234
+ if not @camera.support_formats.any? {|x| x.fcc == "MJPG"}
235
+ raise("#{$target} is not support Motion-JPEG")
236
+ end
219
237
 
220
- if not @camera.support_formats.any? {|x| x.fcc == "MJPG"}
221
- raise("#{$target} is not support Motion-JPEG")
222
- end
238
+ begin
239
+ @mutex.synchronize {
240
+ @config = load_settings()
223
241
 
224
- @config = load_settings()
242
+ @camera.start
243
+ change_state(:ALIVE)
244
+ }
225
245
 
226
- @camera.start
227
- @state = :BUSY
228
- }
246
+ loop {
247
+ @img_que << @camera.capture
248
+ }
229
249
 
230
- loop {
231
- @img_que << @camera.capture
232
- }
250
+ rescue Stop
251
+ $logger.info("main") {"accept stop request"}
252
+ change_state(:STOP)
233
253
 
234
- rescue Stop
235
- $logger.info("main") {"accept stop request"}
236
- @state = :READY
254
+ rescue Restart
255
+ $logger.info("main") {"restart camera"}
256
+ @camera.stop
257
+ retry
258
+
259
+ rescue => e
260
+ change_state(:ABORT)
261
+ raise(e)
262
+
263
+ ensure
264
+ @camera.stop if (@camera.busy? rescue false)
265
+ end
237
266
 
238
267
  rescue => e
239
268
  $logger.error("main") {"camera error occured (#{e.message})"}
240
- @state = :ABORTED
269
+ change_state(:ABORT)
241
270
 
242
271
  ensure
243
- @camera&.stop if @camera&.busy?
244
272
  @camera&.close
245
273
  @camera = nil
246
-
247
274
  $logger.info("main") {"camera thread stop"}
248
275
  end
249
276
  private :camera_thread
@@ -279,17 +306,17 @@ module IPCam
279
306
  end
280
307
 
281
308
  def get_ident_string
282
- raise("state violation") if @state != :BUSY
309
+ raise("state violation") if @state != :ALIVE
283
310
  return "#{@camera.name}@#{@camera.bus}"
284
311
  end
285
312
 
286
313
  def get_config
287
- raise("state violation") if @state != :BUSY
314
+ raise("state violation") if @state != :ALIVE
288
315
  return @config
289
316
  end
290
317
 
291
318
  def set_image_size(width, height)
292
- raise("state violation") if @state != :BUSY
319
+ raise("state violation") if @state != :ALIVE
293
320
 
294
321
  @mutex.synchronize {
295
322
  @config[:image_width] = width
@@ -301,7 +328,7 @@ module IPCam
301
328
  end
302
329
 
303
330
  def set_framerate(num, deno)
304
- raise("state violation") if @state != :BUSY
331
+ raise("state violation") if @state != :ALIVE
305
332
 
306
333
  @mutex.synchronize {
307
334
  @config[:framerate] = [num, deno]
@@ -312,7 +339,7 @@ module IPCam
312
339
  end
313
340
 
314
341
  def set_control(id, val)
315
- raise("state violation") if @state != :BUSY
342
+ raise("state violation") if @state != :ALIVE
316
343
 
317
344
  entry = nil
318
345
 
@@ -328,9 +355,12 @@ module IPCam
328
355
  end
329
356
 
330
357
  def save_config
358
+ $logger.info('main') {"save config to #{$db_file.to_s}"}
331
359
  @mutex.synchronize {
332
360
  $db_file.binwrite(@db.to_msgpack)
333
361
  }
362
+
363
+ broadcast(:save_complete)
334
364
  end
335
365
 
336
366
  def add_client(que)
data/lib/ipcam/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module IPCam
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -95,6 +95,9 @@ module IPCam
95
95
  EOT
96
96
 
97
97
  port << data
98
+
99
+ # データ詰まりを防ぐ為にキューをクリア
100
+ queue.clean
98
101
  }
99
102
  end
100
103
  end
@@ -104,7 +107,7 @@ module IPCam
104
107
  scss name.to_sym, :views => APP_RESOURCE_DIR + "scss"
105
108
  end
106
109
 
107
- get %r{/(css|js|fonts)/(.+)} do |type, name|
110
+ get %r{/(css|js|img|fonts)/(.+)} do |type, name|
108
111
  path = find_resource(type, name)
109
112
 
110
113
  if path
Binary file
@@ -165,7 +165,7 @@ if (!msgpack) {
165
165
  };
166
166
 
167
167
  this.sock.onclose = () => {
168
- if (this.onSessionClosed) this.onSessionClosed();
168
+ this[callback]('session_closed', []);
169
169
  this.sock = null;
170
170
  };
171
171
  }
@@ -131,5 +131,15 @@
131
131
 
132
132
  $text.remove();
133
133
  }
134
+
135
+ static showAbortShield(html) {
136
+ $('body').css('overflow', 'hidden');
137
+
138
+ $('#abort-shield')
139
+ .find('p')
140
+ .html(html)
141
+ .end()
142
+ .fadeIn();
143
+ }
134
144
  }
135
145
  })();
@@ -0,0 +1,44 @@
1
+ @charset "UTF-8";
2
+
3
+ div#abort-shield {
4
+ display: none;
5
+ position: fixed;
6
+ pointer-events: none;
7
+ top: 0;
8
+ left: 0;
9
+ right: 0;
10
+ bottom: 0;
11
+ margin: auto;
12
+ padding: 20px;
13
+ width: 100%;
14
+ height: 100%;
15
+ background-color: rgba(0,0,0,0.6);
16
+ text-align: center;
17
+ z-index: 10000;
18
+
19
+ div.abort-content {
20
+ position: absolute;
21
+ height: fit-content;
22
+ margin: auto;
23
+ top: 0;
24
+ left: 0;
25
+ right: 0;
26
+ bottom: 0;
27
+
28
+ img {
29
+ text-align: center;
30
+ filter: blur(0.25px);
31
+ }
32
+
33
+ p {
34
+ //font-family: Roboto;
35
+ font-weight: lighter;
36
+ font-size: 200%;
37
+ text-align: center;
38
+ }
39
+
40
+ * {
41
+ color: white;
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ <div id="abort-shield" style="display:none;">
2
+ <div class="abort-content">
3
+ <img src="/img/kaiju.png"></img>
4
+ <p id="abort-message"></p>
5
+ </div>
6
+ </div>
@@ -18,6 +18,7 @@
18
18
  var session;
19
19
  var capabilities;
20
20
  var controls;
21
+ var sliders;
21
22
 
22
23
  var imageWidth;
23
24
  var imageHeight;
@@ -30,42 +31,58 @@
30
31
  * declar functions
31
32
  */
32
33
 
33
- function setCameraInfo(info) {
34
+ function setDeviceFile(name) {
35
+ $('h3#device-file').text(name);
36
+ }
37
+
38
+ function setState(state) {
34
39
  var fg;
35
40
  var bg;
41
+ var lb;
36
42
 
37
- $('h3#device-file').text(info["device"]);
38
-
39
- switch (info["state"]) {
40
- case "READY":
43
+ switch (state) {
44
+ case "STOP":
41
45
  default:
42
46
  fg = "royalblue";
43
47
  bg = "white";
48
+ lb = "START";
44
49
  break;
45
50
 
46
- case "BUSY":
47
- fg = "gold";
51
+ case "ALIVE":
52
+ fg = "springgreen";
48
53
  bg = "black";
54
+ lb = "STOP";
49
55
  break;
50
56
 
51
57
  case "ABORT":
52
58
  fg = "crimson";
53
59
  bg = "white";
60
+ lb = "RECOVER";
61
+ break;
54
62
  }
55
63
 
64
+ $('button#action').text(lb);
65
+
56
66
  $('div#state')
57
67
  .css('color', fg)
58
68
  .css('-webkit-text-stroke', `0.5px ${bg}`)
59
- .text(info["state"]);
69
+ .text(state);
60
70
  }
61
71
 
62
- function setIdentString(str) {
72
+ function setupScreenSize() {
63
73
  var height;
64
74
 
65
- $('h5#device-name').text(str);
66
-
67
75
  height = $('body').height() - $('div.jumbotron').outerHeight(true);
68
76
  $('div#main-area').height(height);
77
+
78
+ setTimeout(() => {
79
+ $('div#preview').getNiceScroll().resize();
80
+ $('div#config').getNiceScroll().resize();
81
+ }, 0);
82
+ }
83
+
84
+ function setIdentString(str) {
85
+ $('h6#device-name').text(str);
69
86
  }
70
87
 
71
88
  function sortCapabilities() {
@@ -113,37 +130,43 @@
113
130
  return `${ret} fps`;
114
131
  }
115
132
 
116
- function chooseFramerate(rate) {
117
- var info;
133
+ function findCapability() {
134
+ var ret;
135
+
136
+ ret = capabilities.find((obj) => {
137
+ return ((obj.width == imageWidth) && (obj.height == imageHeight));
138
+ });
139
+
140
+ return ret;
141
+ }
142
+
143
+ function selectFramerate(rates) {
118
144
  var list;
119
145
  var targ;
120
146
 
121
- targ = rate[0] / rate[1];
147
+ targ = framerate[0] / framerate[1];
122
148
 
123
- info = capabilities.find((obj) => {
124
- return ((obj.width == imageWidth) && (obj.height == imageHeight));
125
- });
149
+ if (!rates) {
150
+ rates = findCapability()["rate"];
151
+ }
152
+
153
+ list = rates.concat();
126
154
 
127
- list = info["rate"].reduce((m, n) => {m.push(n); return m}, []);
128
155
  list.sort((a, b) => {
129
156
  return Math.abs((a[0] / a[1]) - targ) - Math.abs((b[0] / b[1]) - targ);
130
157
  });
131
158
 
132
- return `${list[0][0]},${list[0][1]}`;
159
+ $('select#framerate').val(`${list[0][0]},${list[0][1]}`);
133
160
  }
134
161
 
135
162
  function setFramerateSelect() {
136
163
  var info;
137
164
 
138
- /*
139
- * フレームレート
140
- */
165
+ $('select#framerate')
166
+ .empty()
167
+ .off('change');
141
168
 
142
- $('select#framerate').empty();
143
-
144
- info = capabilities.find((obj) => {
145
- return ((obj.width == imageWidth) && (obj.height == imageHeight));
146
- });
169
+ info = findCapability();
147
170
 
148
171
  if (info) {
149
172
  info["rate"].forEach((obj) => {
@@ -154,8 +177,9 @@
154
177
  );
155
178
  });
156
179
 
180
+ selectFramerate(info['rate']);
181
+
157
182
  $('select#framerate')
158
- .val(chooseFramerate(framerate))
159
183
  .on('change', (e) => {;
160
184
  let val;
161
185
  let res;
@@ -174,8 +198,7 @@
174
198
 
175
199
  $input = $('<input>')
176
200
  .attr('id', `control-${info["id"]}`)
177
- .attr('type', 'range')
178
- .attr('value', info["value"]);
201
+ .attr('type', 'range');
179
202
 
180
203
  $('div#controls')
181
204
  .append($('<div>')
@@ -193,17 +216,19 @@
193
216
 
194
217
  tics = info["max"] - info["min"];
195
218
 
196
- $input
219
+ sliders[info["id"]] = $input
197
220
  .ionRangeSlider({
198
- type: "single",
199
- min: info["min"],
200
- max: info["max"],
201
- step: info["step"],
202
- skin: "sharp",
203
- keyboard: true,
204
- hide_min_max: true,
205
- grid: true,
206
- grid_num: (tics > 10)? 10: tics,
221
+ type: "single",
222
+ min: info["min"],
223
+ max: info["max"],
224
+ step: info["step"],
225
+ from: info["value"],
226
+ skin: "sharp",
227
+ keyboard: true,
228
+ hide_min_max: true,
229
+ grid: true,
230
+ grid_num: (tics > 10)? 10: tics,
231
+ prettify_separator: ",",
207
232
 
208
233
  onFinish: (data) => {
209
234
  session.setControl(info["id"], data["from"])
@@ -224,14 +249,17 @@
224
249
 
225
250
  $('div#controls')
226
251
  .append($('<div>')
227
- .addClass('pretty p-default my-2')
228
- .append($input)
252
+ .addClass('form-group')
229
253
  .append($('<div>')
230
- .addClass('state p-primary')
231
- .append($('<label>')
232
- .addClass('form-label')
233
- .attr("for", `control-${info["id"]}`)
234
- .text(info["name"])
254
+ .addClass('pretty p-default my-2')
255
+ .append($input)
256
+ .append($('<div>')
257
+ .addClass('state p-primary')
258
+ .append($('<label>')
259
+ .addClass('form-label')
260
+ .attr("for", `control-${info["id"]}`)
261
+ .text(info["name"])
262
+ )
235
263
  )
236
264
  )
237
265
  );
@@ -291,6 +319,8 @@
291
319
 
292
320
  $('div#controls').append($("<hr>"));
293
321
 
322
+ sliders = {};
323
+
294
324
  info.forEach((entry) => {
295
325
  if (entry["type"] == "integer") {
296
326
  addIntegerForm(entry);
@@ -302,7 +332,9 @@
302
332
  previewCanvas.width = imageWidth;
303
333
  previewCanvas.height = imageHeight;
304
334
 
305
- $('div#preview').getNiceScroll().resize();
335
+ setTimeout(() => {
336
+ $('div#preview').getNiceScroll().resize();
337
+ }, 0);
306
338
  }
307
339
 
308
340
  function updatePreviewCanvas(img) {
@@ -317,29 +349,122 @@
317
349
  imageHeight);
318
350
  }
319
351
 
352
+ function updateImage(data) {
353
+ Utils.loadImageFromData(data)
354
+ .then((img) => {
355
+ updatePreviewCanvas(img);
356
+ })
357
+ .fail((error) => {
358
+ console.log(error);
359
+ });
360
+ }
361
+
362
+ function updateImageSize(width, height) {
363
+ imageWidth = width;
364
+ imageHeight = height;
365
+
366
+ resizePreviewCanvas();
367
+ setFramerateSelect();
368
+ }
369
+
370
+ function updateFramerate(num, deno) {
371
+ framerate = [num, deno]
372
+ selectFramerate();
373
+ }
374
+
375
+ function updateControl(id, val) {
376
+ var info;
377
+
378
+ info = controls.find((obj) => obj["id"] == id);
379
+
380
+ switch (info["type"]) {
381
+ case "boolean":
382
+ $(`input#control-${id}`).prop('checked', val);
383
+ break;
384
+
385
+ case "integer":
386
+ $(`input#control-${id}`).data('ionRangeSlider').update({from:val});
387
+ break;
388
+
389
+ case "menu":
390
+ $(`select#control-${id}`).val(val);
391
+ break;
392
+ }
393
+
394
+ }
395
+
396
+ function changeState(state) {
397
+ var pr1;
398
+ var pr2;
399
+
400
+ setState(state);
401
+
402
+ if (state == "ALIVE") {
403
+ pr1 = session.getIdentString()
404
+ .then((str) => {
405
+ setIdentString(str);
406
+ });
407
+
408
+ pr2 = session.getConfig()
409
+ .then((info) => {
410
+ let rates;
411
+
412
+ capabilities = info["capabilities"];
413
+ controls = info["controls"];
414
+ imageWidth = info["image_width"];
415
+ imageHeight = info["image_height"];
416
+ framerate = info["framerate"];
417
+
418
+ resizePreviewCanvas();
419
+ sortCapabilities();
420
+ setImageSizeSelect();
421
+ setFramerateSelect();
422
+
423
+ setControlForm(info["controls"]);
424
+
425
+ setTimeout(() => {
426
+ $('div#config').getNiceScroll().resize();
427
+ }, 0);
428
+ });
429
+
430
+ $.when(pr1, pr2)
431
+ .done(() => {
432
+ setupScreenSize();
433
+ });
434
+
435
+ } else {
436
+ $('h6#device-name').text(null);
437
+
438
+ $('select#image-size > option').remove();
439
+ $('select#framerate > option').remove();
440
+ $('div#controls').empty();
441
+
442
+ setupScreenSize();
443
+ }
444
+ }
445
+
320
446
  function startSession() {
321
447
  session
322
448
  .on('update_image', (data) => {
323
- Utils.loadImageFromData(data)
324
- .then((img) => {
325
- updatePreviewCanvas(img);
326
- })
327
- .fail((error) => {
328
- console.log(error);
329
- });
449
+ updateImage(data);
330
450
  })
331
451
  .on('update_image_size', (width, height) => {
332
- imageWidth = width;
333
- imageHeight = height;
334
-
335
- resizePreviewCanvas();
336
- setFramerateSelect();
452
+ updateImageSize(width, height);
337
453
  })
338
454
  .on('update_framerate', (num, deno) => {
339
- console.log("not implemented yet");
455
+ updateFramerate(num, deno);
340
456
  })
341
457
  .on('update_control', (id, val) => {
342
- console.log("not implemented yet");
458
+ updateControl(id, val);
459
+ })
460
+ .on('change_state', (state) => {
461
+ changeState(state);
462
+ })
463
+ .on('save_complete', () => {
464
+ $('#save-complete-toast').toast('show');
465
+ })
466
+ .on('session_closed', () => {
467
+ Utils.showAbortShield("session closed");
343
468
  });
344
469
 
345
470
  session.start()
@@ -347,40 +472,13 @@
347
472
  return session.getCameraInfo();
348
473
  })
349
474
  .then((info) => {
350
- setCameraInfo(info);
351
-
352
- if (info["state"] == "BUSY") {
353
- session.getIdentString()
354
- .then((str) => {
355
- setIdentString(str);
356
- });
357
-
358
- session.getConfig()
359
- .then((info) => {
360
- let rates;
361
-
362
- capabilities = info["capabilities"];
363
- controls = info["controls"];
364
- imageWidth = info["image_width"];
365
- imageHeight = info["image_height"];
366
- framerate = info["framerate"];
367
-
368
- sortCapabilities();
369
-
370
- resizePreviewCanvas();
371
- setImageSizeSelect();
372
- setFramerateSelect();
373
-
374
- setControlForm(info["controls"]);
375
-
376
- $('div#config').getNiceScroll().resize();
377
- });
378
- }
475
+ setDeviceFile(info["device"]);
476
+ changeState(info["state"]);
379
477
 
380
478
  return session.addNotifyRequest();
381
479
  })
382
480
  .fail((error) => {
383
- console.log(error);
481
+ Utils.showAbortShield(error);
384
482
  });
385
483
  }
386
484
 
@@ -414,6 +512,7 @@
414
512
 
415
513
  url = `${location.protocol}//${location.host}/stream`;
416
514
  Utils.copyToClipboard(url);
515
+ $('#url-copied-toast').toast('show');
417
516
  });
418
517
  }
419
518
 
@@ -421,6 +520,7 @@
421
520
  session = new Session(WS_URL);
422
521
  capabilities = null;
423
522
  controls = null;
523
+ sliders = null;
424
524
  imageWidth = null;
425
525
  imageHeight = null;
426
526
  framerate = null;
@@ -462,6 +562,11 @@
462
562
  });
463
563
  });
464
564
 
565
+ $(window)
566
+ .on('resize', () => {
567
+ setupScreenSize();
568
+ });
569
+
465
570
  /* デフォルトではコンテキストメニューをOFF */
466
571
  $(document)
467
572
  .on('contextmenu', (e) => {
@@ -1,9 +1,22 @@
1
1
  @charset "UTF-8";
2
+ @import "../../../common/scss/abort_shield.scss";
2
3
 
3
4
  body {
5
+ position: relative;
4
6
  width: 100%;
5
7
  height: 100%;
6
8
 
9
+ div.toast {
10
+ position: fixed;
11
+ top: 1.0rem;
12
+ right: 1.0rem;
13
+ min-width: 20rem;
14
+
15
+ div.toast-body {
16
+ background-color: lightgray;
17
+ }
18
+ }
19
+
7
20
  div.jumbotron {
8
21
  padding: 2rem 1rem;
9
22
  margin-bottom: 0px;
@@ -12,7 +25,7 @@ body {
12
25
  font-weight: bold;
13
26
  }
14
27
 
15
- h5 {
28
+ h6 {
16
29
  font-weight: lighter;
17
30
  }
18
31
  }
@@ -19,14 +19,14 @@
19
19
  <div class="jumbotron jumbotron-fluid">
20
20
  <div class="container">
21
21
  <h3 id="device-file"></h3>
22
- <h5 id="device-name"></h3>
22
+ <h6 id="device-name"></h3>
23
23
  </div>
24
24
  </div>
25
25
 
26
26
  <div id="main-area" class="d-flex d-flex-row">
27
27
  <div id="switches">
28
28
  <div class="form-grounp mt-3">
29
- <button class="btn btn-primary" disabled>STOP</button>
29
+ <button id="action" class="btn btn-primary" disabled>STOP</button>
30
30
  </div>
31
31
 
32
32
  <div class="form-grounp mt-3">
@@ -64,5 +64,9 @@
64
64
  </div>
65
65
  </div>
66
66
  </div>
67
+
68
+ <%= render :erb, :"toast/save_complete" %>
69
+ <%= render :erb, :"toast/url_copied" %>
70
+ <%= render :erb, :"../../common/views/abort_shield" %>
67
71
  </body>
68
72
  </html>
@@ -0,0 +1,15 @@
1
+ <div id="save-complete-toast" class="toast"
2
+ data-delay="3000" role="alert" aria-live="assertive" aria-atomic="true">
3
+ <div class="toast-header">
4
+ <span class="text-primary">&#x25a0; </span>
5
+ <strong class="mr-auto">Notice</strong>
6
+ <button type="button" class="ml-2 mb-1 close"
7
+ data-dismiss="toast" aria-label="Close">
8
+ <span aria-hidden="true">&times;</span>
9
+ </button>
10
+ </div>
11
+
12
+ <div class="toast-body">
13
+ Configuration data saved.
14
+ </div>
15
+ </div>
@@ -0,0 +1,15 @@
1
+ <div id="url-copied-toast" class="toast"
2
+ data-delay="3000" role="alert" aria-live="assertive" aria-atomic="true">
3
+ <div class="toast-header">
4
+ <span class="text-primary">&#x25a0; </span>
5
+ <strong class="mr-auto">Notice</strong>
6
+ <button type="button" class="ml-2 mb-1 close"
7
+ data-dismiss="toast" aria-label="Close">
8
+ <span aria-hidden="true">&times;</span>
9
+ </button>
10
+ </div>
11
+
12
+ <div class="toast-body">
13
+ It had copied streaming URL to clipboard.
14
+ </div>
15
+ </div>
data/run.sh CHANGED
@@ -1,3 +1,3 @@
1
1
  #! /bin/sh
2
2
 
3
- ruby -Ilib bin/ipcam --develop-mode
3
+ ruby -Ilib bin/ipcam --develop-mode $*
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ipcam
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hirosho Kuwagata
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-10 00:00:00.000000000 Z
11
+ date: 2019-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -184,8 +184,11 @@ files:
184
184
  - lib/ipcam/version.rb
185
185
  - lib/ipcam/webserver.rb
186
186
  - lib/ipcam/websock.rb
187
+ - resource/common/img/kaiju.png
187
188
  - resource/common/js/msgpack-rpc.js
188
189
  - resource/common/js/util.js
190
+ - resource/common/scss/abort_shield.scss
191
+ - resource/common/views/abort_shield.erb
189
192
  - resource/extern/css/bootstrap.min.css
190
193
  - resource/extern/css/ion.rangeSlider.min.css
191
194
  - resource/extern/css/pretty-checkbox.min.css
@@ -200,6 +203,8 @@ files:
200
203
  - resource/ipcam/scss/main/style.scss
201
204
  - resource/ipcam/views/main.erb
202
205
  - resource/ipcam/views/settings.erb
206
+ - resource/ipcam/views/toast/save_complete.erb
207
+ - resource/ipcam/views/toast/url_copied.erb
203
208
  - run.sh
204
209
  homepage: https://github.com/kwgt/ipcam
205
210
  licenses: