luci-app-ustreamer: add package

this is a ~replacement for mjpeg-streamer which
is no longer maintained.

See also:
https://github.com/openwrt/packages/pull/28344 drop mjpeg-streamer
https://github.com/openwrt/packages/pull/28472 add ustreamer
https://github.com/openwrt/packages/pull/28528 ustreamer 6.52

Closes #8221

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
Paul Donald
2026-02-09 04:18:35 +01:00
parent a0bad5ea0a
commit b971eaf27a
45 changed files with 567 additions and 298 deletions
@@ -1,264 +0,0 @@
'use strict';
'require view';
'require form';
'require uci';
'require ui';
'require poll';
/* Copyright 2014 Roger D < rogerdammit@gmail.com>
Licensed to the public under the Apache License 2.0. */
return view.extend({
load: function () {
var self = this;
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;}',
]),
);
return Promise.all([uci.load('mjpg-streamer')]);
},
render: function () {
let m, s, o;
m = new form.Map('mjpg-streamer', 'MJPG-streamer', _('mjpg streamer is a streaming application for Linux-UVC compatible webcams'));
//General settings
var section_gen = m.section(form.TypedSection, 'mjpg-streamer', _('General'));
section_gen.addremove = false;
section_gen.anonymous = true;
var enabled = section_gen.option(form.Flag, 'enabled', _('Enabled'), _('Enable MJPG-streamer'));
var input = section_gen.option(form.ListValue, 'input', _('Input plugin'));
input.depends('enabled', '1');
input.value('uvc', 'UVC');
// input: value("file", "File")
input.optional = false;
var output = section_gen.option(form.ListValue, 'output', _('Output plugin'));
output.depends('enabled', '1');
output.value('http', 'HTTP');
output.value('file', 'File');
output.optional = false;
//Plugin settings
s = m.section(form.TypedSection, 'mjpg-streamer', _('Plugin settings'));
s.addremove = false;
s.anonymous = true;
s.tab('output_http', _('HTTP output'));
s.tab('output_file', _('File output'));
s.tab('input_uvc', _('UVC input'));
// s: tab("input_file", _("File input"))
// Input UVC settings
var this_tab = 'input_uvc';
var device = s.taboption(this_tab, form.Value, 'device', _('Device'));
device.default = '/dev/video0';
//device.datatype = "device"
device.value('/dev/video0', '/dev/video0');
device.value('/dev/video1', '/dev/video1');
device.value('/dev/video2', '/dev/video2');
device.optional = false;
var resolution = s.taboption(this_tab, form.Value, 'resolution', _('Resolution'));
resolution.default = '640x480';
resolution.value('320x240', '320x240');
resolution.value('640x480', '640x480');
resolution.value('800x600', '800x600');
resolution.value('864x480', '864x480');
resolution.value('960x544', '960x544');
resolution.value('960x720', '960x720');
resolution.value('1280x720', '1280x720');
resolution.value('1280x960', '1280x960');
resolution.value('1920x1080', '1920x1080');
resolution.optional = true;
var fps = s.taboption(this_tab, form.Value, 'fps', _('Frames per second'));
fps.datatype = 'and(uinteger, min(1))';
fps.placeholder = '5';
fps.optional = true;
var yuv = s.taboption(this_tab, form.Flag, 'yuv', _('Enable YUYV format'), _('Automatic disabling of MJPEG mode'));
var quality = s.taboption(
this_tab,
form.Value,
'quality',
_('JPEG compression quality'),
_('Set the quality in percent. This setting activates YUYV format, disables MJPEG'),
);
quality.datatype = 'range(0, 100)';
var minimum_size = s.taboption(
this_tab,
form.Value,
'minimum_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'),
);
minimum_size.datatype = 'uinteger';
var no_dynctrl = s.taboption(this_tab, form.Flag, 'no_dynctrl', _("Don't initialize dynctrls"), _('Do not initialize dynctrls of Linux-UVC driver'));
var led = s.taboption(this_tab, form.ListValue, 'led', _('Led control'));
led.value('on', _('On'));
led.value('off', _('Off'));
led.value('blink', _('Blink'));
led.value('auto', _('Auto'));
led.optional = true;
// Output HTTP settings
this_tab = 'output_http';
var port = s.taboption(this_tab, form.Value, 'port', _('Port'), _('TCP port for this HTTP server'));
port.datatype = 'port';
port.placeholder = '8080';
var enable_auth = s.taboption(this_tab, form.Flag, 'enable_auth', _('Authentication required'), _('Ask for username and password on connect'));
enable_auth.default = false;
var username = s.taboption(this_tab, form.Value, 'username', _('Username'));
username.depends('enable_auth', '1');
username.optional = false;
var password = s.taboption(this_tab, form.Value, 'password', _('Password'));
password.depends('enable_auth', '1');
password.password = true;
password.optional = false;
password.default = false;
var www = s.taboption(this_tab, form.Value, 'www', _('WWW folder'), _('Folder that contains webpages'));
www.datatype = 'directory';
www.default = '/www/webcam/';
www.optional = false;
function init_stream() {
console.log('init_stream');
start_stream();
}
function _start_stream() {
console.log('_start_stream');
var port = uci.get('mjpg-streamer', 'core', 'port');
if (uci.get('mjpg-streamer', 'core', 'enable_auth') == '1') {
var user = uci.get('mjpg-streamer', 'core', 'username');
var pass = uci.get('mjpg-streamer', 'core', 'password');
var login = user + ':' + pass + '@';
} else {
var login = '';
}
var img = document.getElementById('video_preview') || video_preview;
img.src = 'http://' + login + location.hostname + ':' + port + '/?action=snapshot' + '&t=' + new Date().getTime();
}
function start_stream() {
console.log('start_stream');
setTimeout(function () {
_start_stream();
}, 500);
}
function on_error() {
console.log('on_error');
var img = video_preview;
img.style.display = 'none';
var stream_stat = document.getElementById('stream_status') || stream_status;
stream_stat.style.display = 'block';
start_stream();
}
function on_load() {
console.log('on_load');
var img = video_preview;
img.style.display = 'block';
var stream_stat = stream_status;
stream_stat.style.display = 'none';
}
//HTTP preview
var video_preview = E('img', {
'id': 'video_preview',
'class': 'img-preview',
'error': on_error,
'load': on_load,
});
var stream_status = E(
'p',
{
'id': 'stream_status',
'style': 'text-align: center; color: orange; font-weight: bold;',
},
_('Stream unavailable'),
);
init_stream();
var 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');
//Output file settings
this_tab = 'output_file';
var folder = s.taboption(this_tab, form.Value, 'folder', _('Folder'), _('Set folder to save pictures'));
folder.placeholder = '/tmp/images';
folder.datatype = 'directory';
//mjpeg=s.taboption(this_tab, Value, "mjpeg", _("Mjpeg output"), _("Check to save the stream to an mjpeg file"))
var delay = s.taboption(this_tab, form.Value, 'delay', _('Interval between saving pictures'), _('Set the interval in millisecond'));
delay.placeholder = '5000';
delay.datatype = 'uinteger';
var ringbuffer = s.taboption(this_tab, form.Value, 'ringbuffer', _('Ring buffer size'), _('Max. number of pictures to hold'));
ringbuffer.placeholder = '10';
ringbuffer.datatype = 'uinteger';
var exceed = s.taboption(this_tab, form.Value, 'exceed', _('Exceed'), _('Allow ringbuffer to exceed limit by this amount'));
exceed.datatype = 'uinteger';
var command = s.taboption(
this_tab,
form.Value,
'command',
_('Command to run'),
_('Execute command after saving picture. Mjpg-streamer parses the filename as first parameter to your script.'),
);
var link = s.taboption(this_tab, form.Value, 'link', _('Link newest picture to fixed file name'), _('Link the last picture in ringbuffer to fixed named file provided.'));
return m.render();
},
});
@@ -1,17 +0,0 @@
{
"admin/services/mjpg-streamer": {
"title": "MJPG-streamer",
"action": {
"type": "view",
"path": "mjpg-streamer/mjpg-streamer"
},
"depends": {
"acl": [
"luci-app-mjpg-streamer"
],
"uci": {
"mjpg-streamer": true
}
}
}
}
@@ -1,15 +0,0 @@
{
"luci-app-mjpg-streamer": {
"description": "Grant UCI access for luci-app-mjpg-streamer",
"read": {
"uci": [
"mjpg-streamer"
]
},
"write": {
"uci": [
"mjpg-streamer"
]
}
}
}
@@ -6,12 +6,14 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=MJPG-Streamer service configuration module
LUCI_DEPENDS:=+luci-base +mjpg-streamer
LUCI_TITLE:=ustreamer service configuration module
LUCI_DEPENDS:=+luci-base +ustreamer
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Jo-Philipp Wich <jo@mein.io>
PROVIDES:=luci-app-mjpeg-streamer
include ../../luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,528 @@
'use strict';
'require form';
'require fs';
'require poll';
'require uci';
'require ui';
'require view';
/* Licensed to the public under the Apache License 2.0. */
return view.extend({
load() {
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;}',
]),
);
return Promise.all([
L.resolveDefault(fs.list('/dev/'), []).then(entries => entries.filter(e => /^video.*$/.test(e.name)) ),
uci.load('ustreamer'),
]);
},
render([video_devs]) {
let m, s, o;
let self = this;
poll.add(() => {
self.load().then(([video_devs]) => {
self.render([video_devs]);
});
}, 5);
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.'));
//General settings
const section_gen = m.section(form.TypedSection, 'ustreamer', _('General'));
section_gen.addremove = false;
section_gen.anonymous = true;
const enabled = section_gen.option(form.Flag, 'enabled', _('Enabled'));
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'));
//Plugin settings
s = m.section(form.TypedSection, 'ustreamer', _('Plugin settings'));
s.addremove = true;
s.anonymous = true;
s.tab('h264_sink', _('H264 sink'));
s.tab('output_http', _('HTTP output'));
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';
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;
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.Flag, 'input', _('Input'));
input.default = input.disabled;
const resolution = s.taboption(this_tab, form.Value, 'resolution', _('Resolution'));
resolution.placeholder = '640x480';
resolution.value('320x240', '320x240');
resolution.value('640x480', '640x480');
resolution.value('800x600', '800x600');
resolution.value('864x480', '864x480');
resolution.value('960x544', '960x544');
resolution.value('960x720', '960x720');
resolution.value('1280x720', '1280x720');
resolution.value('1280x960', '1280x960');
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';
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 quality = s.taboption(
this_tab,
form.Value,
'quality',
_('Quality'),
_('Set the quality in percent.'),
);
quality.datatype = 'range(0, 100)';
const allow_truncated_frames = s.taboption(this_tab, form.Flag, 'allow_truncated_frames', _('Allow truncated frames'));
allow_truncated_frames.default = allow_truncated_frames.disabled;
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 persistent = s.taboption(this_tab, form.Flag, 'persistent', _('Persistent'),
_("Don't re-initialize device on timeout. Default: disabled."));
persistent.default = persistent.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. Default: disabled."));
dv_timings.default = dv_timings.disabled;
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');
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 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))';
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))';
workers.placeholder = '2';
workers.optional = true;
const m2m_device = s.taboption(this_tab, form.FileUpload, 'm2m_device', _('M2M device'));
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.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';
min_frame_size.placeholder = '128';
const device_error_delay = s.taboption(this_tab, form.Value, 'device_error_delay', _('Device error delay'));
device_error_delay.datatype = 'and(uinteger, min(1))';
device_error_delay.placeholder = '1';
device_error_delay.optional = true;
// Output HTTP settings
this_tab = 'output_http';
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;
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 username = s.taboption(this_tab, form.Value, 'user', _('Username'));
username.depends('enable_auth', '1');
username.optional = false;
const password = s.taboption(this_tab, form.Value, 'pass', _('Password'));
password.depends('enable_auth', '1');
password.password = true;
password.optional = false;
password.default = false;
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', _('Socket'), _('Folder that contains the socket'));
unix.datatype = 'file';
unix.placeholder = '/path/to/socket';
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';
function init_stream() {
console.debug('init_stream');
start_stream();
}
function _start_stream() {
console.debug('_start_stream');
const port = uci.get('ustreamer', 'core', 'port');
let login;
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 = '';
}
const img = document.getElementById('video_preview') || video_preview;
img.src = 'http://' + login + location.hostname + ':' + port + '/?action=snapshot' + '&t=' + new Date().getTime();
}
function start_stream() {
console.debug('start_stream');
setTimeout(function () {
_start_stream();
}, 5000);
}
function on_error() {
console.warn('on_error');
const img = video_preview;
img.style.display = 'none';
const stream_stat = document.getElementById('stream_status') || stream_status;
stream_stat.style.display = 'block';
// start_stream();
}
function on_load() {
console.debug('on_load');
const img = video_preview;
img.style.display = 'block';
const stream_stat = stream_status;
stream_stat.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'),
);
init_stream();
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.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';
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';
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';
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;
// RAW sink settings
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. Default: disabled.') + '<br/>' +
_('The name should end with a suffix ".raw".') + '<br/>' +
_('Default: disabled.'));
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';
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';
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';
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;
// H264 sink settings
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. Default: disabled.') + '<br/>' +
_('The name should end with a suffix ".h264"') + '<br/>' +
_('Default: disabled.'));
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';
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', _('Sink client TTL'),
_('Client TTL. Default: 10.'));
h264_sink_client_ttl.datatype = 'uinteger';
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';
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';
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';
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.'));
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.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;
// Image control settings
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 brightness = s.taboption(this_tab, form.Value, 'brightness', _('Brightness'));
brightness.placeholder = '128 | auto';
brightness.optional = true;
const contrast = s.taboption(this_tab, form.Value, 'contrast', _('Contrast'));
contrast.placeholder = '128';
contrast.optional = true;
const saturation = s.taboption(this_tab, form.Value, 'saturation', _('Saturation'));
saturation.placeholder = '128';
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'));
gamma.placeholder = '128';
gamma.optional = true;
const sharpness = s.taboption(this_tab, form.Value, 'sharpness', _('Sharpness'));
sharpness.placeholder = '128';
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 white_balance = s.taboption(this_tab, form.Value, 'white_balance', _('White balance'));
white_balance.placeholder = '128 | auto';
white_balance.optional = true;
const gain = s.taboption(this_tab, form.Value, 'gain', _('Gain'));
gain.placeholder = '128 | auto';
gain.optional = true;
const color_effect = s.taboption(this_tab, form.Value, 'color_effect', _('Color effect'));
color_effect.placeholder = '128';
color_effect.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.Flag, 'flip_horizontal', _('Flip horizontally'));
flip_horizontal.default = flip_horizontal.disabled;
const flip_vertical = s.taboption(this_tab, form.Flag, 'flip_vertical', _('Flip vertically'));
flip_vertical.default = flip_vertical.disabled;
return m.render();
},
});
@@ -0,0 +1,17 @@
{
"admin/services/ustreamer": {
"title": "ustreamer",
"action": {
"type": "view",
"path": "ustreamer/ustreamer"
},
"depends": {
"acl": [
"luci-app-ustreamer"
],
"uci": {
"ustreamer": true
}
}
}
}
@@ -0,0 +1,18 @@
{
"luci-app-ustreamer": {
"description": "Grant UCI access for luci-app-ustreamer",
"read": {
"file": [
"/dev/*"
],
"uci": [
"ustreamer"
]
},
"write": {
"uci": [
"ustreamer"
]
}
}
}