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 +4 -4
- data/README.md +10 -0
- data/bin/ipcam +0 -1
- data/lib/ipcam/main.rb +64 -34
- data/lib/ipcam/version.rb +1 -1
- data/lib/ipcam/webserver.rb +4 -1
- data/resource/common/img/kaiju.png +0 -0
- data/resource/common/js/msgpack-rpc.js +1 -1
- data/resource/common/js/util.js +10 -0
- data/resource/common/scss/abort_shield.scss +44 -0
- data/resource/common/views/abort_shield.erb +6 -0
- data/resource/ipcam/js/main.js +197 -92
- data/resource/ipcam/scss/main/style.scss +14 -1
- data/resource/ipcam/views/main.erb +6 -2
- data/resource/ipcam/views/toast/save_complete.erb +15 -0
- data/resource/ipcam/views/toast/url_copied.erb +15 -0
- data/run.sh +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 113a2a9bb9fd4e1a578898654c0a82926abc332d02aea58567e9530ed59b7c37
|
4
|
+
data.tar.gz: fbd86fd23d34fedc6e491bed2d823e03d8f5cc5afd4993a85d1ff3613a203f7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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 = :
|
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(
|
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
|
173
|
-
@db
|
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
|
-
@
|
218
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
238
|
+
begin
|
239
|
+
@mutex.synchronize {
|
240
|
+
@config = load_settings()
|
223
241
|
|
224
|
-
|
242
|
+
@camera.start
|
243
|
+
change_state(:ALIVE)
|
244
|
+
}
|
225
245
|
|
226
|
-
|
227
|
-
|
228
|
-
|
246
|
+
loop {
|
247
|
+
@img_que << @camera.capture
|
248
|
+
}
|
229
249
|
|
230
|
-
|
231
|
-
|
232
|
-
|
250
|
+
rescue Stop
|
251
|
+
$logger.info("main") {"accept stop request"}
|
252
|
+
change_state(:STOP)
|
233
253
|
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
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 != :
|
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 != :
|
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 != :
|
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 != :
|
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 != :
|
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
data/lib/ipcam/webserver.rb
CHANGED
@@ -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
|
data/resource/common/js/util.js
CHANGED
@@ -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
|
+
}
|
data/resource/ipcam/js/main.js
CHANGED
@@ -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
|
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
|
-
|
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 "
|
47
|
-
fg = "
|
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(
|
69
|
+
.text(state);
|
60
70
|
}
|
61
71
|
|
62
|
-
function
|
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
|
117
|
-
var
|
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 =
|
147
|
+
targ = framerate[0] / framerate[1];
|
122
148
|
|
123
|
-
|
124
|
-
|
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
|
-
|
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
|
-
|
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:
|
199
|
-
min:
|
200
|
-
max:
|
201
|
-
step:
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
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('
|
228
|
-
.append($input)
|
252
|
+
.addClass('form-group')
|
229
253
|
.append($('<div>')
|
230
|
-
.addClass('
|
231
|
-
.append($
|
232
|
-
|
233
|
-
.
|
234
|
-
.
|
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
|
-
|
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
|
-
|
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
|
-
|
333
|
-
imageHeight = height;
|
334
|
-
|
335
|
-
resizePreviewCanvas();
|
336
|
-
setFramerateSelect();
|
452
|
+
updateImageSize(width, height);
|
337
453
|
})
|
338
454
|
.on('update_framerate', (num, deno) => {
|
339
|
-
|
455
|
+
updateFramerate(num, deno);
|
340
456
|
})
|
341
457
|
.on('update_control', (id, val) => {
|
342
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
<
|
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">■ </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">×</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">■ </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">×</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
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.
|
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-
|
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:
|