luci-app-ustreamer: add package
authorPaul Donald <redacted>
Mon, 9 Feb 2026 03:18:35 +0000 (04:18 +0100)
committerPaul Donald <redacted>
Mon, 9 Feb 2026 03:18:35 +0000 (04:18 +0100)
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 <redacted>
45 files changed:
applications/luci-app-mjpg-streamer/htdocs/luci-static/resources/view/mjpg-streamer/mjpg-streamer.js [deleted file]
applications/luci-app-mjpg-streamer/root/usr/share/luci/menu.d/luci-app-mjpg-streamer.json [deleted file]
applications/luci-app-mjpg-streamer/root/usr/share/rpcd/acl.d/luci-app-mjpg-streamer.json [deleted file]
applications/luci-app-ustreamer/Makefile [moved from applications/luci-app-mjpg-streamer/Makefile with 72% similarity]
applications/luci-app-ustreamer/htdocs/luci-static/resources/view/ustreamer/ustreamer.js [new file with mode: 0644]
applications/luci-app-ustreamer/po/ar/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ar/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/bg/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/bg/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/bn_BD/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/bn_BD/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ca/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ca/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/cs/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/cs/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/da/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/da/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/de/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/de/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/el/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/el/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/es/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/es/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/et/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/et/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/fi/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/fi/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/fr/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/fr/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ga/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ga/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/he/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/he/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/hi/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/hi/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/hu/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/hu/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/it/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/it/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ja/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ja/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ko/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ko/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/lt/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/lt/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/mr/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/mr/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ms/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ms/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/nb_NO/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/nb_NO/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/nl/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/nl/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/pl/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/pl/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/pt/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/pt/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/pt_BR/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/pt_BR/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ro/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ro/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ru/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ru/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/sk/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/sk/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/sv/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/sv/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/ta/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/ta/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/templates/ustreamer.pot [moved from applications/luci-app-mjpg-streamer/po/templates/mjpg-streamer.pot with 100% similarity]
applications/luci-app-ustreamer/po/tr/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/tr/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/uk/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/uk/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/vi/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/vi/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/zh_Hans/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/zh_Hans/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/po/zh_Hant/ustreamer.po [moved from applications/luci-app-mjpg-streamer/po/zh_Hant/mjpg-streamer.po with 100% similarity]
applications/luci-app-ustreamer/root/usr/share/luci/menu.d/luci-app-ustreamer.json [new file with mode: 0644]
applications/luci-app-ustreamer/root/usr/share/rpcd/acl.d/luci-app-ustreamer.json [new file with mode: 0644]

diff --git a/applications/luci-app-mjpg-streamer/htdocs/luci-static/resources/view/mjpg-streamer/mjpg-streamer.js b/applications/luci-app-mjpg-streamer/htdocs/luci-static/resources/view/mjpg-streamer/mjpg-streamer.js
deleted file mode 100644 (file)
index f4533f7..0000000
+++ /dev/null
@@ -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();
-       },
-});
diff --git a/applications/luci-app-mjpg-streamer/root/usr/share/luci/menu.d/luci-app-mjpg-streamer.json b/applications/luci-app-mjpg-streamer/root/usr/share/luci/menu.d/luci-app-mjpg-streamer.json
deleted file mode 100644 (file)
index 9a5a8bd..0000000
+++ /dev/null
@@ -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
-                       }
-               }
-       }
-}
diff --git a/applications/luci-app-mjpg-streamer/root/usr/share/rpcd/acl.d/luci-app-mjpg-streamer.json b/applications/luci-app-mjpg-streamer/root/usr/share/rpcd/acl.d/luci-app-mjpg-streamer.json
deleted file mode 100644 (file)
index 4a2f1df..0000000
+++ /dev/null
@@ -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"
-                       ]
-               }
-       }
-}
\ No newline at end of file
similarity index 72%
rename from applications/luci-app-mjpg-streamer/Makefile
rename to applications/luci-app-ustreamer/Makefile
index fd536350aa13eed4400a446ef0cc4d7e170ae564..b161b277019faefd9efa8a8ab6b85a33ce7d0089 100644 (file)
@@ -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
diff --git a/applications/luci-app-ustreamer/htdocs/luci-static/resources/view/ustreamer/ustreamer.js b/applications/luci-app-ustreamer/htdocs/luci-static/resources/view/ustreamer/ustreamer.js
new file mode 100644 (file)
index 0000000..cc98bed
--- /dev/null
@@ -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();
+       },
+});
diff --git a/applications/luci-app-ustreamer/root/usr/share/luci/menu.d/luci-app-ustreamer.json b/applications/luci-app-ustreamer/root/usr/share/luci/menu.d/luci-app-ustreamer.json
new file mode 100644 (file)
index 0000000..ad68670
--- /dev/null
@@ -0,0 +1,17 @@
+{
+       "admin/services/ustreamer": {
+               "title": "ustreamer",
+               "action": {
+                       "type": "view",
+                       "path": "ustreamer/ustreamer"
+               },
+               "depends": {
+                       "acl": [
+                               "luci-app-ustreamer"
+                       ],
+                       "uci": {
+                               "ustreamer": true
+                       }
+               }
+       }
+}
diff --git a/applications/luci-app-ustreamer/root/usr/share/rpcd/acl.d/luci-app-ustreamer.json b/applications/luci-app-ustreamer/root/usr/share/rpcd/acl.d/luci-app-ustreamer.json
new file mode 100644 (file)
index 0000000..a62f4af
--- /dev/null
@@ -0,0 +1,18 @@
+{
+       "luci-app-ustreamer": {
+               "description": "Grant UCI access for luci-app-ustreamer",
+               "read": {
+                       "file": [
+                               "/dev/*"
+                       ],
+                       "uci": [
+                               "ustreamer"
+                       ]
+               },
+               "write": {
+                       "uci": [
+                               "ustreamer"
+                       ]
+               }
+       }
+}
\ No newline at end of file
git clone https://git.99rst.org/PROJECT