'use strict';
'require form';
-'require fs';
'require poll';
'require uci';
'require ui';
'require view';
-/* Licensed to the public under the Apache License 2.0. */
+
+/* Licensed to the public under the Apache License 2.0. */
return view.extend({
load() {
+ let self = this;
+ self.stream = { ts: new Date().getTime(), timer: 0 };
+
+ poll.add(function () {
+ self.render();
+ }, 5);
+
document
.querySelector('head')
.appendChild(
E('style', { type: 'text/css' }, [
'.img-preview {display: inline-block !important;height: auto;width: 640px;padding: 4px;line-height: 1.428571429;background-color: #fff;border: 1px solid #ddd;border-radius: 4px;-webkit-transition: all .2s ease-in-out;transition: all .2s ease-in-out;margin-bottom: 5px;display: none;}',
+ '@media (prefers-color-scheme: dark){ .img-preview {background-color: #222;border-color: #333;} }',
]),
);
return Promise.all([
- L.resolveDefault(fs.list('/dev/'), []).then(entries => entries.filter(e => /^video.*$/.test(e.name)) ),
uci.load('ustreamer'),
]);
},
- render([video_devs]) {
+
+ render() {
let m, s, o;
+ let stream = this.stream;
- let self = this;
- poll.add(() => {
- self.load().then(([video_devs]) => {
- self.render([video_devs]);
- });
- }, 5);
+ m = new form.Map(
+ 'ustreamer', 'µStreamer', _('Lightweight and fast MJPEG-HTTP streamer')
+ );
+
+
+ // preview
+
+ function stream_url() {
+ let login = '';
+ let user = uci.get('ustreamer', 'video0', 'user');
+ let pass = uci.get('ustreamer', 'video0', 'pass');
+ let port = uci.get('ustreamer', 'video0', 'port');
- m = new form.Map('ustreamer', 'ustreamer',
- _('µStreamer is a lightweight and very quick server to stream MJPEG video from any V4L2 device to the net.'));
+ if (port == undefined) {
+ port = '8080';
+ }
+
+ if (user != undefined) {
+ if (pass == undefined) {
+ login = user + '@';
+ }
+ else {
+ login = user + ':' + pass + '@';
+ }
+ }
- //General settings
+ return 'http://' + login + location.hostname + ':' + port + '/';
+ }
+
+ const url = stream_url();
- const section_gen = m.section(form.TypedSection, 'ustreamer', _('General'));
- section_gen.addremove = false;
- section_gen.anonymous = true;
+ const stream_link = E('a', {
+ 'id': 'stream_link',
+ 'target': '_blank',
+ 'href': url,
+ },
+ _('Preview'),
+ );
- const enabled = section_gen.option(form.Flag, 'enabled', _('Enabled'));
+ const s_preview = m.section(form.TypedSection, 'ustreamer', stream_link);
+ s_preview.addremove = false;
+ s_preview.anonymous = true;
- const log_level = section_gen.option(form.Value, 'log_level', _('Log level'));
- log_level.placeholder = _('info');
- log_level.value('0', _('info'));
- log_level.value('1', _('performance'));
- log_level.value('2', _('verbose'));
- log_level.value('3', _('debug'));
+ function _start_stream() {
+ let ts = new Date().getTime();
+ let dt = ts - stream.ts;
+ stream.ts = ts;
+ stream.timer = 0;
+ console.log('_start_stream ' + dt);
+
+ let img = document.getElementById('video_preview') || video_preview;
+ img.src = url + 'snapshot' + '?t=' + new Date().getTime();
+ }
- //Plugin settings
+ function start_stream() {
+ if (stream.timer) {
+ return;
+ }
- s = m.section(form.TypedSection, 'ustreamer', _('Plugin settings'));
- s.addremove = true;
+ stream.timer = setTimeout(function () {
+ _start_stream();
+ }, 500);
+ }
+
+ function on_error() {
+ console.log('on_error');
+
+ let img = document.getElementById('video_preview') || video_preview;
+ img.style.display = 'none';
+
+ let status = document.getElementById('stream_status') || stream_status;
+ status.style.display = 'block';
+
+ let enabled = uci.get('ustreamer', 'video0', 'enabled');
+
+ if (enabled) {
+ start_stream();
+ }
+ }
+
+ function on_load() {
+ console.log('on_load');
+
+ let img = document.getElementById('video_preview') || video_preview;
+ img.style.display = 'block';
+
+ let status = document.getElementById('stream_status') || stream_status;
+ status.style.display = 'none';
+ }
+
+ // HTTP preview
+ const video_preview = E('img', {
+ 'id': 'video_preview',
+ 'class': 'img-preview',
+ 'error': on_error,
+ 'load': on_load,
+ }
+ );
+
+ const stream_status = E('p', {
+ 'id': 'stream_status',
+ 'style': 'text-align: center; color: orange; font-weight: bold;',
+ },
+ _('Stream unavailable'),
+ );
+
+ start_stream();
+
+ const preview = s_preview.option(form.DummyValue, '_dummy');
+
+ preview.render = L.bind(function (view, section_id) {
+ return E([], [
+ video_preview,
+ stream_status
+ ]);
+ }, preview, this);
+
+ preview.depends('enabled', '1');
+
+
+ // settings
+
+ s = m.section(form.TypedSection, 'ustreamer', _('Settings'));
+ s.addremove = false;
s.anonymous = true;
- s.tab('h264_sink', _('H264 sink'));
- s.tab('output_http', _('HTTP output'));
+ s.tab('general', _('General'));
+ s.tab('capture', _('Capture'));
+ s.tab('server_http', _('HTTP server'));
+ s.tab('sink_jpeg', _('JPEG sink'));
+ s.tab('sink_raw', _('RAW sink'));
+ s.tab('sink_h264', _('H264 sink'));
+ s.tab('logging', _('Logging'));
s.tab('image_control', _('Image control'));
- s.tab('jpeg_sink', _('JPEG sink'));
- s.tab('raw_sink', _('RAW sink'));
- s.tab('input_uvc', _('UVC input'));
- // Input UVC settings
- let this_tab = 'input_uvc';
+ // general
+
+ let this_tab = 'general';
+
+ const enabled = s.taboption(
+ this_tab, form.Flag, 'enabled', _('Enabled'), _('Enable µStreamer')
+ );
+
+ const device = s.taboption(
+ this_tab, form.Value, 'device', _('Device'),
+ _('Path to V4L2 device. Default: /dev/video0'));
+
+ device.default = '/dev/video0';
+ device.value('/dev/video0', '/dev/video0');
+ device.value('/dev/video1', '/dev/video1');
+ device.value('/dev/video2', '/dev/video2');
+ device.optional = true;
+
+ const device_timeout = s.taboption(
+ this_tab, form.Value, 'device_timeout', _('Device timeout'),
+ _('Timeout for device querying. Default: 1 second'));
- const device = s.taboption(this_tab, form.Value, 'device', _('Device'));
- device.placeholder = '/dev/video0';
- for (const dev of video_devs)
- device.value(`/dev/${dev.name}`);
- device.optional = false;
- device.rmempty = false;
+ device_timeout.datatype = 'and(uinteger, range(0, 60))';
+ device_timeout.placeholder = '5';
+ device_timeout.optional = true;
- const dtimeout = s.taboption(this_tab, form.Value, 'timeout', _('Timeout'), _('units: seconds'));
- dtimeout.placeholder = '5';
- dtimeout.datatype = 'uinteger';
+ const input = s.taboption(
+ this_tab, form.Value, 'input', _('Input'), _('Input channel. Default: 0')
+ );
+
+ input.datatype = 'and(uinteger, range(0, 128))';
+ input.placeholder = '0';
+ input.optional = true;
- const input = s.taboption(this_tab, form.Flag, 'input', _('Input'));
- input.default = input.disabled;
+ const resolution = s.taboption(
+ this_tab, form.Value, 'resolution', _('Resolution'),
+ _('Initial image resolution. Default: 640x480'));
- const resolution = s.taboption(this_tab, form.Value, 'resolution', _('Resolution'));
- resolution.placeholder = '640x480';
+ resolution.default = '640x480';
resolution.value('320x240', '320x240');
resolution.value('640x480', '640x480');
resolution.value('800x600', '800x600');
resolution.value('1920x1080', '1920x1080');
resolution.optional = true;
- const fps = s.taboption(this_tab, form.Value, 'desired_fps', _('Frames per second'),
- _('Default: maximum possible.'));
- fps.datatype = 'and(uinteger, min(1))';
- fps.placeholder = '5';
+ const fps = s.taboption(
+ this_tab, form.Value, 'desired_fps', _('Frames per second'),
+ _('Desired FPS. Default: maximum possible'));
+
+ fps.datatype = 'and(uinteger, min(0))';
+ fps.placeholder = '0';
fps.optional = true;
- const format = s.taboption(this_tab, form.Value, 'format', _('Format'));
- format.placeholder = 'YUYV';
- format.value('BGR24');
- format.value('GREY');
- format.value('JPEG');
- format.value('MJPEG');
- format.value('RGB24');
- format.value('RGB565');
- format.value('UYVY');
- format.value('YUV420');
- format.value('YUYV');
- format.value('YVU420');
- format.value('YVYU');
-
- const encoder = s.taboption(this_tab, form.Value, 'encoder', _('Encoder'));
- encoder.value('CPU');
- encoder.value('HW');
+ const slowdown = s.taboption(
+ this_tab, form.Flag, 'slowdown', _('Slowdown'),
+ _('Slowdown capturing to 1 FPS or less when no stream or sink clients are connected.') + '<br />' +
+ _('Useful to reduce CPU consumption.') + ' ' + _('Default: disabled'));
- const quality = s.taboption(
- this_tab,
- form.Value,
- 'quality',
- _('Quality'),
- _('Set the quality in percent.'),
+ const format = s.taboption(
+ this_tab, form.Value, 'format', _('Image format'), _('Default: YUYV')
);
- quality.datatype = 'range(0, 100)';
+ format.default = 'YUYV';
+ format.value('YUYV', 'YUYV');
+ format.value('YVYU', 'YVYU');
+ format.value('UYVY', 'UYVY');
+ format.value('YUV420', 'YUV420');
+ format.value('YVU420', 'YVU420');
+ format.value('RGB565', 'RGB565');
+ format.value('RGB24', 'RGB24');
+ format.value('BGR24', 'BGR24');
+ format.value('GREY', 'GREY');
+ format.value('MJPEG', 'MJPEG');
+ format.value('JPEG', 'JPEG');
+ format.optional = true;
+
+ const encoder = s.taboption(
+ this_tab, form.Value, 'encoder', _('Еncoder'),
+ _('Use specified encoder. It may affect the number of workers') + '<br />' +
+ '<li><kbd>CPU ──────── </kbd>' + _('Software MJPEG encoding (default)') + '</li>' +
+ '<li><kbd>HW ───────── </kbd>' + _('Use pre-encoded MJPEG frames directly from camera hardware') + '</li>' +
+ '<li><kbd>M2M-VIDEO ── </kbd>' + _('GPU-accelerated MJPEG encoding using V4L2 M2M video interface') + '</li>' +
+ '<li><kbd>M2M-IMAGE ── </kbd>' + _('GPU-accelerated JPEG encoding using V4L2 M2M image interface') + '</li>'
+ );
- const allow_truncated_frames = s.taboption(this_tab, form.Flag, 'allow_truncated_frames', _('Allow truncated frames'));
- allow_truncated_frames.default = allow_truncated_frames.disabled;
+ encoder.default = 'CPU';
+ encoder.value('CPU', 'CPU');
+ encoder.value('HW', 'HW');
+ encoder.value('M2M-VIDEO', 'M2M-VIDEO');
+ encoder.value('M2M-IMAGE', 'M2M-IMAGE');
+ encoder.optional = true;
- const format_swap_rgb = s.taboption(this_tab, form.Flag, 'format_swap_rgb', _('Format: Swap RGB'),
- _('Enable R-G-B order swapping: RGB to BGR and vice versa.'));
- format_swap_rgb.default = format_swap_rgb.disabled;
+ const quality = s.taboption(
+ this_tab, form.Value, 'quality', _('Quality'),
+ _('Set the quality of JPEG encoding: 1 to 100 (best). Default: 80') + '<br />' +
+ _("If HW encoding is used (JPEG source format), attempts to configure the camera or capture device hardware's internal encoder.") + '<br />' +
+ _('MJPEG will not be recoded to MJPEG to change the quality'));
- const persistent = s.taboption(this_tab, form.Flag, 'persistent', _('Persistent'),
- _("Don't re-initialize device on timeout. Default: disabled."));
- persistent.default = persistent.disabled;
+ quality.datatype = 'and(uinteger, range(0, 100))';
+ quality.placeholder = '0';
+ quality.optional = true;
- const dv_timings = s.taboption(this_tab, form.Flag, 'dv_timings', _('DV Timings'),
- _("Enable DV-timings querying and events processing to automatic resolution change. Default: disabled."));
- dv_timings.default = dv_timings.disabled;
+ const host = s.taboption(
+ this_tab, form.Value, 'host', _('Host'),
+ _('Listen on Hostname or IP. Default: 127.0.0.1'));
- const tv_standard = s.taboption(this_tab, form.Value, 'tv_standard', _('TV standard'));
- tv_standard.placeholder = '';
- tv_standard.value('PAL');
- tv_standard.value('NTSC');
- tv_standard.value('SECAM');
+ host.datatype = 'or(ip4addr, ip6addr, host)';
+ host.placeholder = '::';
+ host.optional = true;
- const io_method = s.taboption(this_tab, form.Value, 'io_method', _('IO method'));
- io_method.placeholder = 'MMAP';
- io_method.value('MMAP');
- io_method.value('USERPTR');
+ const port = s.taboption(
+ this_tab, form.Value, 'port', _('Port'),
+ _('Bind to this TCP port.') + ' ' + _(' Default: 8080'));
- const buffers = s.taboption(this_tab, form.Value, 'buffers', _('Buffers'),
- _('The number of buffers to receive data from the device.') + '<br/>' +
- _('Each buffer may processed using an independent thread.') + '<br/>' +
- _('Default: 3 (the number of CPU cores (but not more than 4) + 1).'));
- buffers.datatype = 'and(uinteger, min(1))';
+ port.datatype = 'port';
+ port.placeholder = '8080';
+ port.optional = true;
+
+ const user = s.taboption(
+ this_tab, form.Value, 'user', _('Username'),
+ _('HTTP basic auth user.') + ' ' + _('Default: disabled'));
+
+ user.datatype = 'string';
+ user.placeholder = '';
+ user.optional = true;
+
+ const pass = s.taboption(
+ this_tab, form.Value, 'pass', _('Password'),
+ _('HTTP basic auth passwd.') + ' ' + _('Default: empty'));
+
+ pass.datatype = 'string';
+ pass.placeholder = '';
+ pass.password = true;
+ pass.optional = true;
+
+
+ // capture
+
+ this_tab = 'capture';
+
+ const allow_truncated_frames = s.taboption(
+ this_tab, form.Flag, 'allow_truncated_frames',
+ _('Allow truncated frames'),
+ _('Allows to handle truncated frames.') + ' ' + _('Default: disabled') + '<br />' +
+ _('Useful if the device produces incorrect but still acceptable frames'));
+
+ const format_swap_rgb = s.taboption(
+ this_tab, form.Flag, 'format_swap_rgb', _('R-G-B order swap'),
+ _('RGB to BGR and vice versa.') + ' ' + _('Default: disabled'));
+
+ const persistent = s.taboption(
+ this_tab, form.Flag, 'persistent', _('Persistent'),
+ _("Don't re-initialize device on timeout.") + ' ' + _('Default: disabled'));
+
+ const dv_timings = s.taboption(
+ this_tab, form.Flag, 'dv_timings', _('DV Timings'),
+ _('Enable DV Timings querying and events processing to automatic resolution change') + '<br />' +
+ _('Default: disabled'));
+
+ const tv_standard = s.taboption(
+ this_tab, form.ListValue, 'tv_standard', _('Force TV standard'),
+ _('Default: disabled'));
+
+ tv_standard.default = '';
+ tv_standard.value('', _('default'));
+ tv_standard.value('PAL', 'PAL');
+ tv_standard.value('NTSC', 'NTSC');
+ tv_standard.value('SECAM', 'SECAM');
+ tv_standard.optional = true;
+
+ const io_method = s.taboption(
+ this_tab, form.ListValue, 'io_method', _('V4L2 IO method'),
+ _('Changing this parameter may increase the performance. Or not.') + '<br />' +
+ _('See kernel documentation. Default: MMAP'));
+
+ io_method.default = '';
+ io_method.value('', _('default'));
+ io_method.value('MMAP', 'MMAP');
+ io_method.value('USERPTR', 'USERPTR');
+ io_method.optional = true;
+
+ const buffers = s.taboption(
+ this_tab, form.Value, 'buffers', _('Buffers'),
+ _('The number of buffers to receive data from the device.') + '<br />' +
+ _('Each buffer may be processed using an independent thread.') + '<br />' +
+ _('Default: 3 (the number of CPU cores (but not more than 4) + 1)'));
+
+ buffers.datatype = 'and(uinteger, range(0, 32))';
buffers.placeholder = '3';
buffers.optional = true;
- const workers = s.taboption(this_tab, form.Value, 'workers', _('Workers'),
- _('The number of worker threads but not more than buffers.') + '<br/>' +
- _('Default: 2 (the number of CPU cores (but not more than 4)).'));
- workers.datatype = 'and(uinteger, min(1))';
+ const workers = s.taboption(
+ this_tab, form.Value, 'workers', _('Workers'),
+ _('The number of worker threads but not more than buffers.') + '<br />' +
+ _('Default: 2 (the number of CPU cores (but not more than 4))'));
+
+ workers.datatype = 'and(uinteger, range(0, 32))';
workers.placeholder = '2';
workers.optional = true;
- const m2m_device = s.taboption(this_tab, form.FileUpload, 'm2m_device', _('M2M device'));
+ const m2m_device = s.taboption(
+ this_tab, form.FileUpload, 'm2m_device', _('M2M device'),
+ _('Path to V4L2 M2M encoder device. Default: auto select'));
+
m2m_device.root_directory = '/dev';
- m2m_device.show_hidden = true;
m2m_device.directory_create = false;
m2m_device.enable_download = false;
m2m_device.enable_upload = false;
m2m_device.enable_remove = false;
+ m2m_device.show_hidden = true;
m2m_device.optional = true;
m2m_device.datatype = 'file';
const min_frame_size = s.taboption(
- this_tab,
- form.Value,
- 'min_frame_size',
- _('Drop frames smaller than this limit'),
- _('Set the minimum size if the webcam produces small-sized garbage frames. May happen under low light conditions'),
- );
- min_frame_size.datatype = 'uinteger';
+ this_tab, form.Value, 'min_frame_size', _('Min frame size'),
+ _('Drop frames smaller than this limit. Useful if the device') + '<br />' +
+ _('produces small-sized garbage frames.') + ' ' + _('Default: 128 bytes'));
+
+ min_frame_size.datatype = 'and(uinteger, range(0, 8192))';
min_frame_size.placeholder = '128';
+ min_frame_size.optional = true;
- const device_error_delay = s.taboption(this_tab, form.Value, 'device_error_delay', _('Device error delay'));
- device_error_delay.datatype = 'and(uinteger, min(1))';
+ const device_error_delay = s.taboption(
+ this_tab, form.Value, 'device_error_delay', _('Device error delay'), _(
+ 'Delay before trying to connect to the device again after an error (timeout for example).') + '<br />' +
+ _('Default: 1 second'));
+
+ device_error_delay.datatype = 'and(uinteger, range(0, 60))';
device_error_delay.placeholder = '1';
device_error_delay.optional = true;
- // Output HTTP settings
- this_tab = 'output_http';
+ // HTTP server
- const host = s.taboption(this_tab, form.Value, 'host', _('Host'), _('TCP host for this HTTP server'));
- host.datatype = 'host';
- host.placeholder = '::';
- host.datatype = 'or(hostname,ipaddr)';
- host.optional = false;
+ this_tab = 'server_http';
- const port = s.taboption(this_tab, form.Value, 'port', _('Port'), _('TCP port for this HTTP server'));
- port.datatype = 'port';
- port.placeholder = '8080';
- port.optional = false;
-
- const enable_auth = s.taboption(this_tab, form.Flag, 'enable_auth', _('Authentication required'), _('Ask for username and password on connect'));
- enable_auth.default = false;
+ const tcp_nodelay = s.taboption(
+ this_tab, form.Flag, 'tcp_nodelay', _('TCP no delay'),
+ _('Set TCP_NODELAY flag to the client /stream socket. Only for TCP socket') + '<br />' +
+ _('Default: disabled'));
- const username = s.taboption(this_tab, form.Value, 'user', _('Username'));
- username.depends('enable_auth', '1');
- username.optional = false;
+ const www = s.taboption(
+ this_tab, form.Value, 'static', _('WWW folder'),
+ _('Path to dir with static files instead of embedded root index page.') + '<br />' +
+ _('Symlinks are not supported for security reasons.') + ' ' + _('Default: disabled'));
- const password = s.taboption(this_tab, form.Value, 'pass', _('Password'));
- password.depends('enable_auth', '1');
- password.password = true;
- password.optional = false;
- password.default = false;
+ www.datatype = 'directory';
+ www.placeholder = '/www/webcam';
+ www.optional = true;
- const staticres = s.taboption(this_tab, form.Value, 'static', _('WWW folder'), _('Folder that contains webpages'));
- staticres.datatype = 'directory';
- staticres.placeholder = '/www/webcam/';
- staticres.optional = false;
+ const unix = s.taboption(
+ this_tab, form.Value, 'unix', _('UNIX socket'),
+ _('Bind to UNIX domain socket.') + ' ' + _('Default: disabled'));
- const unix = s.taboption(this_tab, form.Value, 'unix', _('Socket'), _('Folder that contains the socket'));
unix.datatype = 'file';
unix.placeholder = '/path/to/socket';
+ unix.optional = true;
- const unix_mode = s.taboption(this_tab, form.Value, 'unix_mode', _('Socket Permissions'));
- unix_mode.datatype = 'string';
- unix_mode.placeholder = '660';
-
- const drop_same_frames = s.taboption(this_tab, form.Flag, 'drop_same_frames', _('Drop same frames'));
- drop_same_frames.default = drop_same_frames.disabled;
-
- const fake_resolution = s.taboption(this_tab, form.Value, 'fake_resolution', _('Fake resolution'));
- fake_resolution.placeholder = '640x480';
- fake_resolution.keylist = resolution.keylist;
- fake_resolution.vallist = resolution.vallist;
-
- const allow_origin = s.taboption(this_tab, form.Value, 'allow_origin', _('Allow origin'));
- allow_origin.values = resolution.values;
-
- const instance_id = s.taboption(this_tab, form.Value, 'instance_id', _('Instance ID'));
-
- const server_timeout = s.taboption(this_tab, form.Value, 'server_timeout', _('Server timeout'));
- server_timeout.datatype = 'uinteger';
- server_timeout.placeholder = '10';
+ const unix_rm = s.taboption(
+ this_tab, form.Flag, 'unix_rm', _('UNIX socket remove old'),
+ _('Try to remove old UNIX socket file before binding.') + ' ' + _('Default: disabled'));
-
-
- function init_stream() {
- console.debug('init_stream');
- start_stream();
+ function validate_file_mode (section_id, value) {
+ if (!value || /^[0-7]{3,4}$/.test(value)) return true;
+ return _('Expecting: file mode, e.g. 640 or 0640');
}
- function _start_stream() {
- console.debug('_start_stream');
-
- const port = uci.get('ustreamer', 'core', 'port');
- let login;
+ const unix_mode = s.taboption(
+ this_tab, form.Value, 'unix_mode', _('UNIX socket permissions'),
+ _('Set UNIX socket file permissions (like 777).') + ' ' + _('Default: disabled'));
- if (uci.get('ustreamer', 'core', 'enable_auth') == '1') {
- const user = uci.get('ustreamer', 'core', 'username');
- const pass = uci.get('ustreamer', 'core', 'password');
- login = `${user}:${pass}@`;
- } else {
- login = '';
- }
+ unix_mode.validate = validate_file_mode;
+ unix_mode.placeholder = '660';
+ unix_mode.optional = true;
- const img = document.getElementById('video_preview') || video_preview;
- img.src = 'http://' + login + location.hostname + ':' + port + '/?action=snapshot' + '&t=' + new Date().getTime();
- }
+ const drop_same_frames = s.taboption(
+ this_tab, form.Value, 'drop_same_frames', _('Drop same frames'),
+ _("Don't send identical frames to clients, but no more than specified number.") + '<br />' +
+ _('It can significantly reduce the outgoing traffic, but will increase the CPU load.') + '<br />' +
+ _("Don't use this option with analog signal sources or webcams, it's useless.") + '<br />' +
+ _('Default: disabled'));
- function start_stream() {
- console.debug('start_stream');
+ drop_same_frames.datatype = 'and(uinteger, min(0))';
+ drop_same_frames.placeholder = '0';
+ drop_same_frames.optional = true;
- setTimeout(function () {
- _start_stream();
- }, 5000);
- }
+ const fake_resolution = s.taboption(
+ this_tab, form.Value, 'fake_resolution', _('Fake resolution'),
+ _('Override image resolution for the /state.') + ' ' + _('Default: disabled'));
- function on_error() {
- console.warn('on_error');
+ fake_resolution.default = '';
+ fake_resolution.keylist = resolution.keylist;
+ fake_resolution.vallist = resolution.vallist;
+ fake_resolution.optional = true;
- const img = video_preview;
- img.style.display = 'none';
+ const allow_origin = s.taboption(
+ this_tab, form.Value, 'allow_origin', _('Allow origin'),
+ _('Set Access-Control-Allow-Origin header.') + ' ' + _('Default: disabled'));
- const stream_stat = document.getElementById('stream_status') || stream_status;
- stream_stat.style.display = 'block';
+ allow_origin.datatype = 'string';
+ allow_origin.optional = true;
- // start_stream();
- }
+ const instance_id = s.taboption(
+ this_tab, form.Value, 'instance_id', _('Instance ID'),
+ _('A short string identifier to be displayed in the /state handle.') + '<br />' +
+ _('It must satisfy regexp') + ' <code>^[a-zA-Z0-9./+_-]*$.</code>' + ' ' + _('Default: an empty string'));
- function on_load() {
- console.debug('on_load');
+ instance_id.datatype = 'string';
+ instance_id.optional = true;
- const img = video_preview;
- img.style.display = 'block';
+ const server_timeout = s.taboption(
+ this_tab, form.Value, 'server_timeout', _('Server timeout'),
+ _('Timeout for client connections. Default: 10 seconds'));
- const stream_stat = stream_status;
- stream_stat.style.display = 'none';
- }
+ server_timeout.datatype = 'and(uinteger, range(0, 60))';
+ server_timeout.placeholder = '10';
+ server_timeout.optional = true;
- //HTTP preview
- const video_preview = E('img', {
- 'id': 'video_preview',
- 'class': 'img-preview',
- 'error': on_error,
- 'load': on_load,
- });
- const stream_status = E('p', {
- 'id': 'stream_status',
- 'style': 'text-align: center; color: orange; font-weight: bold;',
- },
- _('Stream unavailable'),
- );
+ // JPEG sink
+ this_tab = 'sink_jpeg';
- init_stream();
+ const jpeg_sink = s.taboption(
+ this_tab, form.Value, 'jpeg_sink', _('JPEG sink'),
+ _('Use the shared memory to sink JPEG frames.') + '<br />' +
+ _('The name should end with a suffix .jpg or .jpeg') + ' ' + _('Default: disabled'));
- const preview = s.taboption(this_tab, form.DummyValue, '_dummy');
- preview.render = L.bind(function (view, section_id) {
- return E([], [
- video_preview,
- stream_status
- ]);
- }, preview, this);
- preview.depends('output', 'http');
-
- // JPEG sink settings
-
- this_tab = 'jpeg_sink';
-
- const jpeg_sink = s.taboption(this_tab, form.Value, 'jpeg_sink', _('JPEG sink'),
- _('Use the shared memory to sink JPEG frames. Default: disabled.') + '<br/>' +
- _('The name should end with a suffix ".jpeg".') + '<br/>' +
- _('Default: disabled.'));
+ jpeg_sink.datatype = 'file';
jpeg_sink.placeholder = 'name.jpeg';
jpeg_sink.optional = true;
- const jpeg_sink_mode = s.taboption(this_tab, form.Value, 'jpeg_sink_mode', _('JPEG sink mode'),
- _('Set JPEG sink permissions (like 777). Default: 660.'));
- jpeg_sink_mode.datatype = 'string';
+ const jpeg_sink_mode = s.taboption(
+ this_tab, form.Value, 'jpeg_sink_mode', _('Sink permissions'),
+ _('Set sink file permissions.') + ' ' + _('Default: 660'));
+
+ jpeg_sink_mode.validate = validate_file_mode;
jpeg_sink_mode.placeholder = '660';
jpeg_sink_mode.optional = true;
- const jpeg_sink_client_ttl = s.taboption(this_tab, form.Value, 'jpeg_sink_client_ttl', _('Client TTL'),
- _('Default: 10.'));
- jpeg_sink_client_ttl.datatype = 'uinteger';
+ const jpeg_sink_client_ttl = s.taboption(
+ this_tab, form.Value, 'jpeg_sink_client_ttl', _('Client TTL'),
+ _('Default: 10 seconds'));
+
+ jpeg_sink_client_ttl.datatype = 'and(uinteger, range(0, 60))';
jpeg_sink_client_ttl.placeholder = '10';
jpeg_sink_client_ttl.optional = true;
- const jpeg_sink_timeout = s.taboption(this_tab, form.Value, 'jpeg_sink_timeout', _('JPEG sink timeout'),
- _('Timeout for lock. Default: 1.'));
- jpeg_sink_timeout.datatype = 'uinteger';
+ const jpeg_sink_timeout = s.taboption(
+ this_tab, form.Value, 'jpeg_sink_timeout', _('Timeout for lock'),
+ _('Default: 1 second'));
+
+ jpeg_sink_timeout.datatype = 'and(uinteger, range(0, 60))';
jpeg_sink_timeout.placeholder = '1';
jpeg_sink_timeout.optional = true;
- const jpeg_sink_rm = s.taboption(this_tab, form.Flag, 'jpeg_sink_rm', _('Remove JPEG sink'),
- _('Remove JPEG sink file on exit'));
- jpeg_sink_rm.default = jpeg_sink_rm.disabled;
+ const jpeg_sink_rm = s.taboption(
+ this_tab, form.Flag, 'jpeg_sink_rm', _('Remove on stop'),
+ _('Remove shared memory on stop.') + ' ' + _('Default: disabled'));
+
+ // RAW sink
- // RAW sink settings
+ this_tab = 'sink_raw';
- this_tab = 'raw_sink';
+ const raw_sink = s.taboption(
+ this_tab, form.Value, 'raw_sink', _('RAW sink'),
+ _('Use the shared memory to sink RAW frames.') + '<br />' +
+ _('The name should end with a suffix .raw') + ' ' + _('Default: disabled'));
- const raw_sink = s.taboption(this_tab, form.Value, 'raw_sink', _('RAW sink'),
- _('Use the shared memory to sink RAW frames. Default: disabled.') + '<br/>' +
- _('The name should end with a suffix ".raw".') + '<br/>' +
- _('Default: disabled.'));
+ raw_sink.datatype = 'file';
raw_sink.placeholder = 'name.raw';
raw_sink.optional = true;
- const raw_sink_mode = s.taboption(this_tab, form.Value, 'raw_sink_mode', _('RAW sink mode'),
- _('Set RAW sink permissions (like 777). Default: 660.'));
- raw_sink_mode.datatype = 'string';
+ const raw_sink_mode = s.taboption(
+ this_tab, form.Value, 'raw_sink_mode', _('Sink permissions'),
+ _('Set sink file permissions.') + ' ' + _('Default: 660'));
+
+ raw_sink_mode.validate = validate_file_mode;
raw_sink_mode.placeholder = '660';
raw_sink_mode.optional = true;
- const raw_sink_client_ttl = s.taboption(this_tab, form.Value, 'raw_sink_client_ttl', _('RAW sink client TTL'),
- _('Client TTL. Default: 10.'));
- raw_sink_client_ttl.datatype = 'uinteger';
+ const raw_sink_client_ttl = s.taboption(
+ this_tab, form.Value, 'raw_sink_client_ttl', _('Client TTL'),
+ _('Default: 10 seconds'));
+
+ raw_sink_client_ttl.datatype = 'and(uinteger, range(0, 60))';
raw_sink_client_ttl.placeholder = '10';
raw_sink_client_ttl.optional = true;
- const raw_sink_timeout = s.taboption(this_tab, form.Value, 'raw_sink_timeout', _('RAW sink timeout'),
- _('Timeout for lock. Default: 1.'));
- raw_sink_timeout.datatype = 'uinteger';
+ const raw_sink_timeout = s.taboption(
+ this_tab, form.Value, 'raw_sink_timeout', _('Timeout for lock'),
+ _('Default: 1 second'));
+
+ raw_sink_timeout.datatype = 'and(uinteger, range(0, 60))';
raw_sink_timeout.placeholder = '1';
raw_sink_timeout.optional = true;
- const raw_sink_rm = s.taboption(this_tab, form.Flag, 'raw_sink_rm', _('Remove RAW sink'),
- _('Remove shared memory on stop. Default: disabled.'));
- raw_sink_rm.default = raw_sink_rm.disabled;
+ const raw_sink_rm = s.taboption(
+ this_tab, form.Flag, 'raw_sink_rm', _('Remove on stop'),
+ _('Remove shared memory on stop. Default: disabled'));
+
+
+ // H264 sink
- // H264 sink settings
+ this_tab = 'sink_h264';
- this_tab = 'h264_sink';
+ const h264_sink = s.taboption(
+ this_tab, form.Value, 'h264_sink', _('H264 sink'),
+ _('Use the shared memory to sink H264 frames.') + '<br />' +
+ _('The name should end with a suffix .h264') + ' ' + _('Default: disabled'));
- const h264_sink = s.taboption(this_tab, form.Value, 'h264_sink', _('H264 sink'),
- _('Use the shared memory to sink H264 frames. Default: disabled.') + '<br/>' +
- _('The name should end with a suffix ".h264"') + '<br/>' +
- _('Default: disabled.'));
+ h264_sink.datatype = 'file';
h264_sink.placeholder = 'name.h264';
h264_sink.optional = true;
- const h264_sink_mode = s.taboption(this_tab, form.Value, 'h264_sink_mode', _('H264 sink mode'),
- _('Set H264 sink permissions (like 777). Default: 660.'));
- h264_sink_mode.datatype = 'string';
+ const h264_sink_mode = s.taboption(
+ this_tab, form.Value, 'h264_sink_mode', _('Sink permissions'),
+ _('Set sink file permissions.') + ' ' + _('Default: 660'));
+
+ h264_sink_mode.validate = validate_file_mode;
h264_sink_mode.placeholder = '660';
h264_sink_mode.optional = true;
- const h264_sink_rm = s.taboption(this_tab, form.Flag, 'h264_sink_rm', _('Remove'),
- _('Remove shared memory on stop. Default: disabled.'));
- h264_sink_rm.default = h264_sink_rm.disabled;
+ const h264_sink_client_ttl = s.taboption(
+ this_tab, form.Value, 'h264_sink_client_ttl', _('Client TTL'),
+ _('Default: 10 seconds'));
- const h264_sink_client_ttl = s.taboption(this_tab, form.Value, 'h264_sink_client_ttl', _('Sink client TTL'),
- _('Client TTL. Default: 10.'));
- h264_sink_client_ttl.datatype = 'uinteger';
+ h264_sink_client_ttl.datatype = 'and(uinteger, range(0, 60))';
h264_sink_client_ttl.placeholder = '10';
h264_sink_client_ttl.optional = true;
- const h264_sink_timeout = s.taboption(this_tab, form.Value, 'h264_sink_timeout', _('Sink timeout'),
- _('Timeout for lock. Default: 1.'));
- h264_sink_timeout.datatype = 'uinteger';
+ const h264_sink_timeout = s.taboption(
+ this_tab, form.Value, 'h264_sink_timeout', _('Timeout for lock'),
+ _('Default: 1 second'));
+
+ h264_sink_timeout.datatype = 'and(uinteger, range(0, 60))';
h264_sink_timeout.placeholder = '1';
h264_sink_timeout.optional = true;
- const h264_bitrate = s.taboption(this_tab, form.Value, 'h264_bitrate', _('Bitrate'),
- _('H264 bitrate in Kbps. Default: 5000.'));
- h264_bitrate.datatype = 'uinteger';
+ const h264_sink_rm = s.taboption(
+ this_tab, form.Flag, 'h264_sink_rm', _('Remove on stop'),
+ _('Remove shared memory on stop.') + ' ' + _('Default: disabled'));
+
+ const h264_boost = s.taboption(
+ this_tab, form.Flag, 'h264_boost', _('H264 boost'),
+ _('Increase encoder performance on PiKVM V4.') + ' ' + _('Default: disabled'));
+
+ const h264_bitrate = s.taboption(
+ this_tab, form.Value, 'h264_bitrate', _('Bitrate (kbps)'),
+ _('Default: 5000 kbps'));
+
+ h264_bitrate.datatype = 'and(uinteger, range(25, 20000))';
h264_bitrate.placeholder = '5000';
h264_bitrate.optional = true;
- const h264_gop = s.taboption(this_tab, form.Value, 'h264_gop', _('H264 GOP'),
- _('Interval between keyframes. Default: 30.'));
- h264_gop.datatype = 'uinteger';
+ const h264_gop = s.taboption(
+ this_tab, form.Value, 'h264_gop', _('Keyframe interval'),
+ _('Default: 30'));
+
+ h264_gop.datatype = 'and(uinteger, range(0, 60))';
h264_gop.placeholder = '30';
h264_gop.optional = true;
- const h264_m2m_device = s.taboption(this_tab, form.FileUpload, 'h264_m2m_device', _('H264 M2M device'),
- _('Path to V4L2 M2M encoder device. Default: auto select.'));
+ const h264_m2m_device = s.taboption(
+ this_tab, form.FileUpload, 'h264_m2m_device', _('M2M device'),
+ _('Path to V4L2 M2M encoder device. Default: auto select'));
+
h264_m2m_device.root_directory = '/dev';
- h264_m2m_device.show_hidden = true;
h264_m2m_device.directory_create = false;
h264_m2m_device.enable_download = false;
h264_m2m_device.enable_upload = false;
h264_m2m_device.enable_remove = false;
+ h264_m2m_device.show_hidden = true;
h264_m2m_device.optional = true;
h264_m2m_device.datatype = 'file';
- const h264_boost = s.taboption(this_tab, form.Flag, 'h264_boost', _('H264 boost'),
- _('Increase encoder performance on PiKVM V4. Default: disabled.'));
- h264_boost.default = h264_boost.disabled;
- const exit_on_no_clients = s.taboption(this_tab, form.Flag, 'exit_on_no_clients', _('Exit on no clients'),
- _('Exit the program if there have been no stream or sink clients ') +
- _('or any HTTP requests in the last N seconds. Default: 0 (disabled).'));
- exit_on_no_clients.default = exit_on_no_clients.disabled;
+ // logging
- // Image control settings
+ this_tab = 'logging';
+
+ const log_level = s.taboption(
+ this_tab, form.ListValue, 'log_level', _('Log level'),
+ _('Verbosity level of messages from 0 (info) to 3 (debug)') + '<br />' +
+ _('Enabling debugging messages can slow down the program') + '<br />' +
+ _('Default: 0 (info)'));
+
+ log_level.default = '';
+ log_level.datatype = 'and(uinteger, range(0, 3))';
+ log_level.value('', _('default'));
+ log_level.value('0', '0 ' + _('Info'));
+ log_level.value('1', '1 ' + _('Performance'));
+ log_level.value('2', '2 ' + _('Verbose'));
+ log_level.value('3', '3 ' + _('Debug'));
+ log_level.optional = true;
+
+ const exit_on_no_clients = s.taboption(
+ this_tab, form.Value, 'exit_on_no_clients', _('Exit on no clients'),
+ _('Exit the program if there have been no stream or sink clients') + '<br />' +
+ _('or any HTTP requests in the last N seconds.') + ' ' + _('Default: 0 (disabled)'));
+
+ exit_on_no_clients.datatype = 'and(uinteger, range(0, 86400))';
+ exit_on_no_clients.placeholder = '0';
+ exit_on_no_clients.optional = true;
+
+
+ // image control
this_tab = 'image_control';
- const image_default = s.taboption(this_tab, form.Flag, 'image_default', _('Use device defaults'));
- image_default.default = image_default.disabled;
+ const image_default = s.taboption(
+ this_tab, form.Flag, 'image_default', _('Image default'),
+ _('Reset all image settings below to default.') + ' ' + _('Unchecked: no change'));
+
+ function validate_int_default (section_id, value) {
+ if (!value || (value == 'default')) return true;
+ value = parseInt(value);
+ if (!isNaN(value)) return true;
+ return _('Expecting: number | default');
+ }
+
+ function validate_int_default_auto (section_id, value) {
+ if (!value || (value == 'default') || (value == 'auto')) return true;
+ value = parseInt(value);
+ if (!isNaN(value)) return true;
+ return _('Expecting: number | default | auto');
+ }
+
+ const brightness = s.taboption(
+ this_tab, form.Value, 'brightness', _('Brightness'),
+ _('number | default | auto. Blank: no change'));
- const brightness = s.taboption(this_tab, form.Value, 'brightness', _('Brightness'));
- brightness.placeholder = '128 | auto';
+ brightness.validate = validate_int_default_auto;
+ brightness.placeholder = '128 | default | auto';
brightness.optional = true;
- const contrast = s.taboption(this_tab, form.Value, 'contrast', _('Contrast'));
- contrast.placeholder = '128';
+ const contrast = s.taboption(
+ this_tab, form.Value, 'contrast', _('Contrast'),
+ _('number | default. Blank: no change'));
+
+ contrast.validate = validate_int_default;
+ contrast.placeholder = '128 | default';
contrast.optional = true;
- const saturation = s.taboption(this_tab, form.Value, 'saturation', _('Saturation'));
- saturation.placeholder = '128';
+ const saturation = s.taboption(
+ this_tab, form.Value, 'saturation', _('Saturation'),
+ _('number | default. Blank: no change'));
+
+ saturation.validate = validate_int_default;
+ saturation.placeholder = '128 | default';
saturation.optional = true;
- const hue = s.taboption(this_tab, form.Value, 'hue', _('Hue'));
- hue.placeholder = '128 | auto';
- hue.optional = true;
+ const gamma = s.taboption(
+ this_tab, form.Value, 'gamma', _('Gamma'),
+ _('number | default. Blank: no change'));
- const gamma = s.taboption(this_tab, form.Value, 'gamma', _('Gamma'));
- gamma.placeholder = '128';
+ gamma.validate = validate_int_default;
+ gamma.placeholder = 'default';
gamma.optional = true;
- const sharpness = s.taboption(this_tab, form.Value, 'sharpness', _('Sharpness'));
- sharpness.placeholder = '128';
+ const gain = s.taboption(
+ this_tab, form.Value, 'gain', _('Gain'),
+ _('number | default | auto. Blank: no change'));
+
+ gain.validate = validate_int_default_auto;
+ gain.placeholder = '0 | default | auto';
+ gain.optional = true;
+
+ const hue = s.taboption(
+ this_tab, form.Value, 'hue', _('Hue'),
+ _('number | default | auto. Blank: no change'));
+
+ hue.validate = validate_int_default_auto;
+ hue.placeholder = 'number | default | auto';
+ hue.optional = true;
+
+ const sharpness = s.taboption(
+ this_tab, form.Value, 'sharpness', _('Sharpness'),
+ _('number | default. Blank: no change'));
+
+ sharpness.validate = validate_int_default;
+ sharpness.placeholder = '128 | default';
sharpness.optional = true;
- const backlight_compensation = s.taboption(this_tab, form.Value, 'backlight_compensation', _('Backlight compensation'));
- backlight_compensation.placeholder = '128';
- backlight_compensation.optional = true;
+ const color_effect = s.taboption(
+ this_tab, form.Value, 'color_effect', _('Colour effect'),
+ _('number | default. Blank: no change'));
- const white_balance = s.taboption(this_tab, form.Value, 'white_balance', _('White balance'));
- white_balance.placeholder = '128 | auto';
+ color_effect.validate = validate_int_default;
+ color_effect.placeholder = 'default';
+ color_effect.optional = true;
+
+ const white_balance = s.taboption(
+ this_tab, form.Value, 'white_balance', _('White balance'),
+ _('temperature | default | auto. Blank: no change'));
+
+ white_balance.validate = validate_int_default_auto;
+ white_balance.placeholder = '4000 | default | auto';
white_balance.optional = true;
- const gain = s.taboption(this_tab, form.Value, 'gain', _('Gain'));
- gain.placeholder = '128 | auto';
- gain.optional = true;
+ const backlight_compensation = s.taboption(
+ this_tab, form.Value, 'backlight_compensation',
+ _('Backlight compensation'),
+ _('number | default. Blank: no change'));
- const color_effect = s.taboption(this_tab, form.Value, 'color_effect', _('Color effect'));
- color_effect.placeholder = '128';
- color_effect.optional = true;
+ backlight_compensation.validate = validate_int_default;
+ backlight_compensation.placeholder = '0 | default';
+ backlight_compensation.optional = true;
- const rotate = s.taboption(this_tab, form.Value, 'rotate', _('Rotate'));
- rotate.datatype = 'uinteger';
- rotate.optional = true;
+ const flip_horizontal = s.taboption(
+ this_tab, form.Value, 'flip_horizontal', _('Flip horizontal'),
+ _('number | default. Blank: no change'));
- const flip_horizontal = s.taboption(this_tab, form.Flag, 'flip_horizontal', _('Flip horizontally'));
- flip_horizontal.default = flip_horizontal.disabled;
+ flip_horizontal.validate = validate_int_default;
+ flip_horizontal.placeholder = '0 | default';
+ flip_horizontal.optional = true;
+
+ const flip_vertical = s.taboption(
+ this_tab, form.Value, 'flip_vertical', _('Flip vertical'),
+ _('number | default. Blank: no change'));
+
+ flip_vertical.validate = validate_int_default;
+ flip_vertical.placeholder = '0 | default';
+ flip_vertical.optional = true;
+
+ const rotate = s.taboption(
+ this_tab, form.Value, 'rotate', _('Rotate'),
+ _('number | default. Blank: no change'));
+
+ rotate.validate = validate_int_default;
+ rotate.placeholder = '0 | default';
+ rotate.optional = true;
- const flip_vertical = s.taboption(this_tab, form.Flag, 'flip_vertical', _('Flip vertically'));
- flip_vertical.default = flip_vertical.disabled;
return m.render();
},