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 +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:
|