ipcam 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +56 -0
- data/COPYRIGHT.md +99 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +59 -0
- data/Rakefile +2 -0
- data/bin/ipcam +129 -0
- data/ipcam.gemspec +50 -0
- data/lib/ipcam/main.rb +344 -0
- data/lib/ipcam/version.rb +3 -0
- data/lib/ipcam/webserver.rb +178 -0
- data/lib/ipcam/websock.rb +380 -0
- data/resource/common/js/msgpack-rpc.js +191 -0
- data/resource/common/js/util.js +135 -0
- data/resource/extern/css/bootstrap.min.css +7 -0
- data/resource/extern/css/ion.rangeSlider.min.css +1 -0
- data/resource/extern/css/pretty-checkbox.min.css +12 -0
- data/resource/extern/js/bootstrap.min.js +7 -0
- data/resource/extern/js/ion.rangeSlider.min.js +2 -0
- data/resource/extern/js/jquery-3.4.1.min.js +2 -0
- data/resource/extern/js/jquery.nicescroll.min.js +2 -0
- data/resource/extern/js/msgpack.min.js +2 -0
- data/resource/extern/js/popper.min.js +5 -0
- data/resource/ipcam/js/main.js +486 -0
- data/resource/ipcam/js/session.js +61 -0
- data/resource/ipcam/scss/main/style.scss +91 -0
- data/resource/ipcam/views/main.erb +68 -0
- data/resource/ipcam/views/settings.erb +19 -0
- data/run.sh +3 -0
- metadata +228 -0
@@ -0,0 +1,486 @@
|
|
1
|
+
/*
|
2
|
+
* Sample for v4l2-ruby
|
3
|
+
*
|
4
|
+
* Copyright (C) 2019 Hiroshi Kuwagata <kgt9221@gmail.com>
|
5
|
+
*/
|
6
|
+
|
7
|
+
(function () {
|
8
|
+
/*
|
9
|
+
* define constants
|
10
|
+
*/
|
11
|
+
|
12
|
+
const WS_URL = `ws://${location.hostname}:${parseInt(location.port)+1}/`;
|
13
|
+
|
14
|
+
/*
|
15
|
+
* declar package global variabled
|
16
|
+
*/
|
17
|
+
|
18
|
+
var session;
|
19
|
+
var capabilities;
|
20
|
+
var controls;
|
21
|
+
|
22
|
+
var imageWidth;
|
23
|
+
var imageHeight;
|
24
|
+
var framerate;
|
25
|
+
|
26
|
+
var previewCanvas;
|
27
|
+
var previewGc;
|
28
|
+
|
29
|
+
/*
|
30
|
+
* declar functions
|
31
|
+
*/
|
32
|
+
|
33
|
+
function setCameraInfo(info) {
|
34
|
+
var fg;
|
35
|
+
var bg;
|
36
|
+
|
37
|
+
$('h3#device-file').text(info["device"]);
|
38
|
+
|
39
|
+
switch (info["state"]) {
|
40
|
+
case "READY":
|
41
|
+
default:
|
42
|
+
fg = "royalblue";
|
43
|
+
bg = "white";
|
44
|
+
break;
|
45
|
+
|
46
|
+
case "BUSY":
|
47
|
+
fg = "gold";
|
48
|
+
bg = "black";
|
49
|
+
break;
|
50
|
+
|
51
|
+
case "ABORT":
|
52
|
+
fg = "crimson";
|
53
|
+
bg = "white";
|
54
|
+
}
|
55
|
+
|
56
|
+
$('div#state')
|
57
|
+
.css('color', fg)
|
58
|
+
.css('-webkit-text-stroke', `0.5px ${bg}`)
|
59
|
+
.text(info["state"]);
|
60
|
+
}
|
61
|
+
|
62
|
+
function setIdentString(str) {
|
63
|
+
var height;
|
64
|
+
|
65
|
+
$('h5#device-name').text(str);
|
66
|
+
|
67
|
+
height = $('body').height() - $('div.jumbotron').outerHeight(true);
|
68
|
+
$('div#main-area').height(height);
|
69
|
+
}
|
70
|
+
|
71
|
+
function sortCapabilities() {
|
72
|
+
capabilities.sort((a, b) => {
|
73
|
+
return ((a.width * a.height) - (b.width * b.height));
|
74
|
+
});
|
75
|
+
|
76
|
+
capabilities.forEach((info) => {
|
77
|
+
info["rate"].sort((a, b) => {return (a[0] / a[1]) - (b[0] / b[1])});
|
78
|
+
});
|
79
|
+
}
|
80
|
+
|
81
|
+
function setImageSizeSelect() {
|
82
|
+
/*
|
83
|
+
* 画像サイズ
|
84
|
+
*/
|
85
|
+
$('select#image-size').empty();
|
86
|
+
|
87
|
+
capabilities.forEach((obj) => {
|
88
|
+
$('select#image-size')
|
89
|
+
.append($('<option>')
|
90
|
+
.attr('value', `${obj.width},${obj.height}`)
|
91
|
+
.text(`${obj.width} \u00d7 ${obj.height}`)
|
92
|
+
);
|
93
|
+
});
|
94
|
+
|
95
|
+
$('select#image-size')
|
96
|
+
.val(`${imageWidth},${imageHeight}`)
|
97
|
+
.on('change', (e) => {
|
98
|
+
let val;
|
99
|
+
let res;
|
100
|
+
|
101
|
+
val = $(e.target).val();
|
102
|
+
res = val.match(/(\d+),(\d+)/);
|
103
|
+
|
104
|
+
session.setImageSize(parseInt(res[1]), parseInt(res[2]));
|
105
|
+
});
|
106
|
+
}
|
107
|
+
|
108
|
+
function framerateString(val) {
|
109
|
+
var ret;
|
110
|
+
|
111
|
+
ret = Math.trunc((val[0] / val[1]) * 100) / 100;
|
112
|
+
|
113
|
+
return `${ret} fps`;
|
114
|
+
}
|
115
|
+
|
116
|
+
function chooseFramerate(rate) {
|
117
|
+
var info;
|
118
|
+
var list;
|
119
|
+
var targ;
|
120
|
+
|
121
|
+
targ = rate[0] / rate[1];
|
122
|
+
|
123
|
+
info = capabilities.find((obj) => {
|
124
|
+
return ((obj.width == imageWidth) && (obj.height == imageHeight));
|
125
|
+
});
|
126
|
+
|
127
|
+
list = info["rate"].reduce((m, n) => {m.push(n); return m}, []);
|
128
|
+
list.sort((a, b) => {
|
129
|
+
return Math.abs((a[0] / a[1]) - targ) - Math.abs((b[0] / b[1]) - targ);
|
130
|
+
});
|
131
|
+
|
132
|
+
return `${list[0][0]},${list[0][1]}`;
|
133
|
+
}
|
134
|
+
|
135
|
+
function setFramerateSelect() {
|
136
|
+
var info;
|
137
|
+
|
138
|
+
/*
|
139
|
+
* フレームレート
|
140
|
+
*/
|
141
|
+
|
142
|
+
$('select#framerate').empty();
|
143
|
+
|
144
|
+
info = capabilities.find((obj) => {
|
145
|
+
return ((obj.width == imageWidth) && (obj.height == imageHeight));
|
146
|
+
});
|
147
|
+
|
148
|
+
if (info) {
|
149
|
+
info["rate"].forEach((obj) => {
|
150
|
+
$('select#framerate')
|
151
|
+
.append($('<option>')
|
152
|
+
.attr('value', `${obj[0]},${obj[1]}`)
|
153
|
+
.text(framerateString(obj))
|
154
|
+
);
|
155
|
+
});
|
156
|
+
|
157
|
+
$('select#framerate')
|
158
|
+
.val(chooseFramerate(framerate))
|
159
|
+
.on('change', (e) => {;
|
160
|
+
let val;
|
161
|
+
let res;
|
162
|
+
|
163
|
+
val = $(e.target).val();
|
164
|
+
res = val.match(/(\d+),(\d+)/)
|
165
|
+
|
166
|
+
session.setFramerate(parseInt(res[1]), parseInt(res[2]));
|
167
|
+
});
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
function addIntegerForm(info) {
|
172
|
+
var $input;
|
173
|
+
var tics;
|
174
|
+
|
175
|
+
$input = $('<input>')
|
176
|
+
.attr('id', `control-${info["id"]}`)
|
177
|
+
.attr('type', 'range')
|
178
|
+
.attr('value', info["value"]);
|
179
|
+
|
180
|
+
$('div#controls')
|
181
|
+
.append($('<div>')
|
182
|
+
.addClass("mb-2")
|
183
|
+
.append($('<label>')
|
184
|
+
.addClass("form-label")
|
185
|
+
.attr("for", `control-${info["id"]}`)
|
186
|
+
.text(info["name"])
|
187
|
+
)
|
188
|
+
.append($('<div>')
|
189
|
+
.addClass('form-group ml-3')
|
190
|
+
.append($input)
|
191
|
+
)
|
192
|
+
);
|
193
|
+
|
194
|
+
tics = info["max"] - info["min"];
|
195
|
+
|
196
|
+
$input
|
197
|
+
.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,
|
207
|
+
|
208
|
+
onFinish: (data) => {
|
209
|
+
session.setControl(info["id"], data["from"])
|
210
|
+
}
|
211
|
+
});
|
212
|
+
}
|
213
|
+
|
214
|
+
function addBooleanForm(info) {
|
215
|
+
var $input;
|
216
|
+
|
217
|
+
$input = $('<input>')
|
218
|
+
.attr('id', `control-${info["id"]}`)
|
219
|
+
.attr('type', 'checkbox')
|
220
|
+
.attr('checked', info["value"])
|
221
|
+
.on('change', (e) => {
|
222
|
+
session.setControl(info["id"], $(e.target).is(':checked'));
|
223
|
+
});
|
224
|
+
|
225
|
+
$('div#controls')
|
226
|
+
.append($('<div>')
|
227
|
+
.addClass('pretty p-default my-2')
|
228
|
+
.append($input)
|
229
|
+
.append($('<div>')
|
230
|
+
.addClass('state p-primary')
|
231
|
+
.append($('<label>')
|
232
|
+
.addClass('form-label')
|
233
|
+
.attr("for", `control-${info["id"]}`)
|
234
|
+
.text(info["name"])
|
235
|
+
)
|
236
|
+
)
|
237
|
+
);
|
238
|
+
}
|
239
|
+
|
240
|
+
function addMenuForm(info) {
|
241
|
+
var $select;
|
242
|
+
|
243
|
+
$select = $('<select>')
|
244
|
+
.attr('id', `control-${info["id"]}`)
|
245
|
+
.addClass('form-control offset-1 col-11');
|
246
|
+
|
247
|
+
for (const [label, value] of Object.entries(info["items"])) {
|
248
|
+
$select
|
249
|
+
.append($('<option>')
|
250
|
+
.attr("value", value)
|
251
|
+
.text(label)
|
252
|
+
);
|
253
|
+
}
|
254
|
+
|
255
|
+
$select
|
256
|
+
.val(info["value"])
|
257
|
+
.on('change', (e) => {
|
258
|
+
session.setControl(info["id"], parseInt($(e.target).val()));
|
259
|
+
});
|
260
|
+
|
261
|
+
$('div#controls')
|
262
|
+
.append($('<div>')
|
263
|
+
.addClass("form-group")
|
264
|
+
.append($('<label>')
|
265
|
+
.addClass('form-label')
|
266
|
+
.attr("for", `control-${info["id"]}`)
|
267
|
+
.text(info["name"])
|
268
|
+
)
|
269
|
+
.append($select)
|
270
|
+
);
|
271
|
+
}
|
272
|
+
|
273
|
+
function setControlForm(info) {
|
274
|
+
$('div#controls').empty();
|
275
|
+
|
276
|
+
$('div#controls').append($("<hr>"));
|
277
|
+
|
278
|
+
info.forEach((entry) => {
|
279
|
+
if (entry["type"] == "boolean") {
|
280
|
+
addBooleanForm(entry);
|
281
|
+
}
|
282
|
+
});
|
283
|
+
|
284
|
+
$('div#controls').append($("<hr>"));
|
285
|
+
|
286
|
+
info.forEach((entry) => {
|
287
|
+
if (entry["type"] == "menu") {
|
288
|
+
addMenuForm(entry);
|
289
|
+
}
|
290
|
+
});
|
291
|
+
|
292
|
+
$('div#controls').append($("<hr>"));
|
293
|
+
|
294
|
+
info.forEach((entry) => {
|
295
|
+
if (entry["type"] == "integer") {
|
296
|
+
addIntegerForm(entry);
|
297
|
+
}
|
298
|
+
});
|
299
|
+
}
|
300
|
+
|
301
|
+
function resizePreviewCanvas() {
|
302
|
+
previewCanvas.width = imageWidth;
|
303
|
+
previewCanvas.height = imageHeight;
|
304
|
+
|
305
|
+
$('div#preview').getNiceScroll().resize();
|
306
|
+
}
|
307
|
+
|
308
|
+
function updatePreviewCanvas(img) {
|
309
|
+
previewGc.drawImage(img,
|
310
|
+
0,
|
311
|
+
0,
|
312
|
+
img.width,
|
313
|
+
img.height,
|
314
|
+
0,
|
315
|
+
0,
|
316
|
+
imageWidth,
|
317
|
+
imageHeight);
|
318
|
+
}
|
319
|
+
|
320
|
+
function startSession() {
|
321
|
+
session
|
322
|
+
.on('update_image', (data) => {
|
323
|
+
Utils.loadImageFromData(data)
|
324
|
+
.then((img) => {
|
325
|
+
updatePreviewCanvas(img);
|
326
|
+
})
|
327
|
+
.fail((error) => {
|
328
|
+
console.log(error);
|
329
|
+
});
|
330
|
+
})
|
331
|
+
.on('update_image_size', (width, height) => {
|
332
|
+
imageWidth = width;
|
333
|
+
imageHeight = height;
|
334
|
+
|
335
|
+
resizePreviewCanvas();
|
336
|
+
setFramerateSelect();
|
337
|
+
})
|
338
|
+
.on('update_framerate', (num, deno) => {
|
339
|
+
console.log("not implemented yet");
|
340
|
+
})
|
341
|
+
.on('update_control', (id, val) => {
|
342
|
+
console.log("not implemented yet");
|
343
|
+
});
|
344
|
+
|
345
|
+
session.start()
|
346
|
+
.then(() => {
|
347
|
+
return session.getCameraInfo();
|
348
|
+
})
|
349
|
+
.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
|
+
}
|
379
|
+
|
380
|
+
return session.addNotifyRequest();
|
381
|
+
})
|
382
|
+
.fail((error) => {
|
383
|
+
console.log(error);
|
384
|
+
});
|
385
|
+
}
|
386
|
+
|
387
|
+
function setupScreen() {
|
388
|
+
$('div#preview')
|
389
|
+
.niceScroll({
|
390
|
+
enablekeyboard: true,
|
391
|
+
zindex: 100,
|
392
|
+
autohidemode: true,
|
393
|
+
horizrailenabled: false
|
394
|
+
});
|
395
|
+
|
396
|
+
$('div#config')
|
397
|
+
.niceScroll({
|
398
|
+
enablekeyboard: true,
|
399
|
+
zindex: 100,
|
400
|
+
autohidemode: true,
|
401
|
+
horizrailenabled: false
|
402
|
+
});
|
403
|
+
}
|
404
|
+
|
405
|
+
function setupButtons() {
|
406
|
+
$('button#save-config')
|
407
|
+
.on('click', () => {
|
408
|
+
session.saveConfig();
|
409
|
+
});
|
410
|
+
|
411
|
+
$('button#copy-url')
|
412
|
+
.on('click', () => {
|
413
|
+
let url;
|
414
|
+
|
415
|
+
url = `${location.protocol}//${location.host}/stream`;
|
416
|
+
Utils.copyToClipboard(url);
|
417
|
+
});
|
418
|
+
}
|
419
|
+
|
420
|
+
function initialize() {
|
421
|
+
session = new Session(WS_URL);
|
422
|
+
capabilities = null;
|
423
|
+
controls = null;
|
424
|
+
imageWidth = null;
|
425
|
+
imageHeight = null;
|
426
|
+
framerate = null;
|
427
|
+
previewCanvas = $('canvas#preview-canvas')[0];
|
428
|
+
previewGc = previewCanvas.getContext('2d');
|
429
|
+
|
430
|
+
setupScreen();
|
431
|
+
setupButtons();
|
432
|
+
|
433
|
+
startSession();
|
434
|
+
}
|
435
|
+
|
436
|
+
/*
|
437
|
+
* set handler for global objects
|
438
|
+
*/
|
439
|
+
|
440
|
+
/* エントリーポイントの設定 */
|
441
|
+
$(window)
|
442
|
+
.on('load', () => {
|
443
|
+
let list = [
|
444
|
+
"/css/bootstrap.min.css",
|
445
|
+
"/css/ion.rangeSlider.min.css",
|
446
|
+
"/css/pretty-checkbox.min.css",
|
447
|
+
|
448
|
+
"/js/popper.min.js",
|
449
|
+
"/js/bootstrap.min.js",
|
450
|
+
"/js/msgpack.min.js",
|
451
|
+
"/js/jquery.nicescroll.min.js",
|
452
|
+
"/js/ion.rangeSlider.min.js",
|
453
|
+
|
454
|
+
"/css/main/style.scss",
|
455
|
+
"/js/msgpack-rpc.js",
|
456
|
+
"/js/session.js",
|
457
|
+
];
|
458
|
+
|
459
|
+
Utils.require(list)
|
460
|
+
.done(() => {
|
461
|
+
initialize();
|
462
|
+
});
|
463
|
+
});
|
464
|
+
|
465
|
+
/* デフォルトではコンテキストメニューをOFF */
|
466
|
+
$(document)
|
467
|
+
.on('contextmenu', (e) => {
|
468
|
+
e.stopPropagation();
|
469
|
+
return false;
|
470
|
+
});
|
471
|
+
|
472
|
+
/* Drop&Dragを無効にしておく */
|
473
|
+
$(document)
|
474
|
+
.on('dragover', (e) => {
|
475
|
+
e.stopPropagation();
|
476
|
+
return false;
|
477
|
+
})
|
478
|
+
.on('dragenter', (e) => {
|
479
|
+
e.stopPropagation();
|
480
|
+
return false;
|
481
|
+
})
|
482
|
+
.on('drop', (e) => {
|
483
|
+
e.stopPropagation();
|
484
|
+
return false;
|
485
|
+
});
|
486
|
+
})();
|
@@ -0,0 +1,61 @@
|
|
1
|
+
/*
|
2
|
+
* Sample for v4l2-ruby
|
3
|
+
*
|
4
|
+
* Copyright (C) 2019 Hiroshi Kuwagata <kgt9221@gmail.com>
|
5
|
+
*/
|
6
|
+
|
7
|
+
if (!msgpack || !msgpack.rpc) {
|
8
|
+
throw "msgpack-lite.js and msgpack-rpc.js is not load yet"
|
9
|
+
}
|
10
|
+
|
11
|
+
(function () {
|
12
|
+
Session = class extends msgpack.rpc {
|
13
|
+
constructor(url) {
|
14
|
+
super(url)
|
15
|
+
}
|
16
|
+
|
17
|
+
hello() {
|
18
|
+
return this.remoteCall('hello');
|
19
|
+
}
|
20
|
+
|
21
|
+
addNotifyRequest() {
|
22
|
+
var args;
|
23
|
+
|
24
|
+
args = Array.prototype.slice.call(arguments);
|
25
|
+
if (args.length == 0) {
|
26
|
+
args = Object.keys(this.handlers);
|
27
|
+
}
|
28
|
+
|
29
|
+
return this.remoteCall('add_notify_request', ...args);
|
30
|
+
}
|
31
|
+
|
32
|
+
getCameraInfo() {
|
33
|
+
return this.remoteCall('get_camera_info');
|
34
|
+
}
|
35
|
+
|
36
|
+
getIdentString() {
|
37
|
+
return this.remoteCall('get_ident_string');
|
38
|
+
}
|
39
|
+
|
40
|
+
getConfig() {
|
41
|
+
return this.remoteCall('get_config');
|
42
|
+
}
|
43
|
+
|
44
|
+
setImageSize(width, height) {
|
45
|
+
return this.remoteCall('set_image_size', width, height);
|
46
|
+
}
|
47
|
+
|
48
|
+
setFramerate(num, deno) {
|
49
|
+
return this.remoteCall('set_framerate', num, deno);
|
50
|
+
}
|
51
|
+
|
52
|
+
setControl(id, val) {
|
53
|
+
return this.remoteCall('set_control', id, val);
|
54
|
+
}
|
55
|
+
|
56
|
+
saveConfig() {
|
57
|
+
return this.remoteCall('save_config');
|
58
|
+
}
|
59
|
+
|
60
|
+
}
|
61
|
+
})();
|
@@ -0,0 +1,91 @@
|
|
1
|
+
@charset "UTF-8";
|
2
|
+
|
3
|
+
body {
|
4
|
+
width: 100%;
|
5
|
+
height: 100%;
|
6
|
+
|
7
|
+
div.jumbotron {
|
8
|
+
padding: 2rem 1rem;
|
9
|
+
margin-bottom: 0px;
|
10
|
+
|
11
|
+
h3 {
|
12
|
+
font-weight: bold;
|
13
|
+
}
|
14
|
+
|
15
|
+
h5 {
|
16
|
+
font-weight: lighter;
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|
20
|
+
div#switches {
|
21
|
+
width: 15%;
|
22
|
+
height: 100%;
|
23
|
+
text-align: center;
|
24
|
+
|
25
|
+
button {
|
26
|
+
width: 90%;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
div#preview {
|
31
|
+
width: 60%;
|
32
|
+
height: 100%;
|
33
|
+
|
34
|
+
text-align: center;
|
35
|
+
position: relative;
|
36
|
+
|
37
|
+
div#preview-wrapper {
|
38
|
+
position: relative;
|
39
|
+
|
40
|
+
div#state {
|
41
|
+
position: absolute;
|
42
|
+
top: 0px;
|
43
|
+
left: 16px;
|
44
|
+
font-weight: bold;
|
45
|
+
font-size: 24pt;
|
46
|
+
}
|
47
|
+
|
48
|
+
canvas {
|
49
|
+
width: 100%;
|
50
|
+
border: solid 1px rgba(0, 0, 0, 0.3);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
div#config {
|
56
|
+
width: 25%;
|
57
|
+
height: 100%;
|
58
|
+
overflow: hidden;
|
59
|
+
|
60
|
+
select {
|
61
|
+
font-size: 80%;
|
62
|
+
}
|
63
|
+
|
64
|
+
.irs {
|
65
|
+
font-family: monospace;
|
66
|
+
}
|
67
|
+
|
68
|
+
.irs--sharp {
|
69
|
+
.irs-bar {
|
70
|
+
background-color: cornflowerblue;
|
71
|
+
}
|
72
|
+
|
73
|
+
.irs-handle {
|
74
|
+
background-color: dodgerblue;
|
75
|
+
}
|
76
|
+
|
77
|
+
.irs-handle > i:first-child {
|
78
|
+
border-top-color: dodgerblue;
|
79
|
+
}
|
80
|
+
|
81
|
+
.irs-min, .irs-max {
|
82
|
+
visibility: hidden;
|
83
|
+
background-color: transparent;
|
84
|
+
}
|
85
|
+
|
86
|
+
.irs-from, .irs-to, .irs-single {
|
87
|
+
background-color: gray;
|
88
|
+
}
|
89
|
+
}
|
90
|
+
}
|
91
|
+
}
|
@@ -0,0 +1,68 @@
|
|
1
|
+
<html>
|
2
|
+
<head>
|
3
|
+
<meta charset="utf-8">
|
4
|
+
<title>Camera</title>
|
5
|
+
|
6
|
+
<% if not $develop_mode %>
|
7
|
+
<meta http-equiv="Pragma" content="no-cache">
|
8
|
+
<meta http-equiv="Cache-Control" content="no-cache">
|
9
|
+
<meta http-equiv="Expires" content="-1">
|
10
|
+
<% end %>
|
11
|
+
</meta>
|
12
|
+
|
13
|
+
<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
|
14
|
+
<script type="text/javascript" src="/js/util.js"></script>
|
15
|
+
<script type="text/javascript" src="/js/main.js"></script>
|
16
|
+
</head>
|
17
|
+
|
18
|
+
<body>
|
19
|
+
<div class="jumbotron jumbotron-fluid">
|
20
|
+
<div class="container">
|
21
|
+
<h3 id="device-file"></h3>
|
22
|
+
<h5 id="device-name"></h3>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
|
26
|
+
<div id="main-area" class="d-flex d-flex-row">
|
27
|
+
<div id="switches">
|
28
|
+
<div class="form-grounp mt-3">
|
29
|
+
<button class="btn btn-primary" disabled>STOP</button>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="form-grounp mt-3">
|
33
|
+
<button id="save-config" class="btn btn-primary">SAVE</button>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div class="form-grounp mt-3">
|
37
|
+
<button id="copy-url" class="btn btn-primary">COPY URL</button>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
|
41
|
+
<div id="preview">
|
42
|
+
<div id="preview-wrapper" class="mx-3 my-3">
|
43
|
+
<div id="state"></div>
|
44
|
+
<canvas id="preview-canvas" width=320 height=240></canvas>
|
45
|
+
</div>
|
46
|
+
</div>
|
47
|
+
|
48
|
+
<div id="config">
|
49
|
+
<div class="mx-3 my-3">
|
50
|
+
<div class="form-group">
|
51
|
+
<label for="image-size">image size</label>
|
52
|
+
<select id="image-size"
|
53
|
+
class="form-control offset-1 col-11"></select>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<div class="form-group">
|
57
|
+
<label for="framerate">framerate</label>
|
58
|
+
<select id="framerate"
|
59
|
+
class="form-control offset-1 col-11"></select>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<div id="controls">
|
63
|
+
</div>
|
64
|
+
</div>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</body>
|
68
|
+
</html>
|