ipcam 0.1.2 → 0.2.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.
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: