'use strict'; 'require form'; 'require poll'; 'require uci'; 'require ui'; 'require view'; /* 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([ uci.load('ustreamer'), ]); }, render() { let m, s, o; let stream = this.stream; 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'); if (port == undefined) { port = '8080'; } if (user != undefined) { if (pass == undefined) { login = user + '@'; } else { login = user + ':' + pass + '@'; } } return 'http://' + login + location.hostname + ':' + port + '/'; } const url = stream_url(); const stream_link = E('a', { 'id': 'stream_link', 'target': '_blank', 'href': url, }, _('Preview'), ); const s_preview = m.section(form.TypedSection, 'ustreamer', stream_link); s_preview.addremove = false; s_preview.anonymous = true; 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(); } function start_stream() { if (stream.timer) { return; } 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('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')); // 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')); device_timeout.datatype = 'and(uinteger, range(0, 60))'; device_timeout.placeholder = '5'; device_timeout.optional = true; 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 resolution = s.taboption( this_tab, form.Value, 'resolution', _('Resolution'), _('Initial image resolution. Default: 640x480')); 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; 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 slowdown = s.taboption( this_tab, form.Flag, 'slowdown', _('Slowdown'), _('Slowdown capturing to 1 FPS or less when no stream or sink clients are connected.') + '
' + _('Useful to reduce CPU consumption.') + ' ' + _('Default: disabled')); const format = s.taboption( this_tab, form.Value, 'format', _('Image format'), _('Default: YUYV') ); 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') + '
' + '
  • CPU ──────── ' + _('Software MJPEG encoding (default)') + '
  • ' + '
  • HW ───────── ' + _('Use pre-encoded MJPEG frames directly from camera hardware') + '
  • ' + '
  • M2M-VIDEO ── ' + _('GPU-accelerated MJPEG encoding using V4L2 M2M video interface') + '
  • ' + '
  • M2M-IMAGE ── ' + _('GPU-accelerated JPEG encoding using V4L2 M2M image interface') + '
  • ' ); 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 quality = s.taboption( this_tab, form.Value, 'quality', _('Quality'), _('Set the quality of JPEG encoding: 1 to 100 (best). Default: 80') + '
    ' + _("If HW encoding is used (JPEG source format), attempts to configure the camera or capture device hardware's internal encoder.") + '
    ' + _('MJPEG will not be recoded to MJPEG to change the quality')); quality.datatype = 'and(uinteger, range(0, 100))'; quality.placeholder = '0'; quality.optional = true; const host = s.taboption( this_tab, form.Value, 'host', _('Host'), _('Listen on Hostname or IP. Default: 127.0.0.1')); host.datatype = 'or(ip4addr, ip6addr, host)'; host.placeholder = '::'; host.optional = true; const port = s.taboption( this_tab, form.Value, 'port', _('Port'), _('Bind to this TCP port.') + ' ' + _(' Default: 8080')); 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') + '
    ' + _('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') + '
    ' + _('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.') + '
    ' + _('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.') + '
    ' + _('Each buffer may be processed using an independent thread.') + '
    ' + _('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.') + '
    ' + _('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'), _('Path to V4L2 M2M encoder device. Default: auto select')); m2m_device.root_directory = '/dev'; 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', _('Min frame size'), _('Drop frames smaller than this limit. Useful if the device') + '
    ' + _('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'), _( 'Delay before trying to connect to the device again after an error (timeout for example).') + '
    ' + _('Default: 1 second')); device_error_delay.datatype = 'and(uinteger, range(0, 60))'; device_error_delay.placeholder = '1'; device_error_delay.optional = true; // HTTP server this_tab = 'server_http'; 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') + '
    ' + _('Default: disabled')); const www = s.taboption( this_tab, form.Value, 'static', _('WWW folder'), _('Path to dir with static files instead of embedded root index page.') + '
    ' + _('Symlinks are not supported for security reasons.') + ' ' + _('Default: disabled')); www.datatype = 'directory'; www.placeholder = '/www/webcam'; www.optional = true; const unix = s.taboption( this_tab, form.Value, 'unix', _('UNIX socket'), _('Bind to UNIX domain socket.') + ' ' + _('Default: disabled')); unix.datatype = 'file'; unix.placeholder = '/path/to/socket'; unix.optional = true; 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 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'); } const unix_mode = s.taboption( this_tab, form.Value, 'unix_mode', _('UNIX socket permissions'), _('Set UNIX socket file permissions (like 777).') + ' ' + _('Default: disabled')); unix_mode.validate = validate_file_mode; unix_mode.placeholder = '660'; unix_mode.optional = true; 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.") + '
    ' + _('It can significantly reduce the outgoing traffic, but will increase the CPU load.') + '
    ' + _("Don't use this option with analog signal sources or webcams, it's useless.") + '
    ' + _('Default: disabled')); drop_same_frames.datatype = 'and(uinteger, min(0))'; drop_same_frames.placeholder = '0'; drop_same_frames.optional = true; const fake_resolution = s.taboption( this_tab, form.Value, 'fake_resolution', _('Fake resolution'), _('Override image resolution for the /state.') + ' ' + _('Default: disabled')); fake_resolution.default = ''; fake_resolution.keylist = resolution.keylist; fake_resolution.vallist = resolution.vallist; fake_resolution.optional = true; const allow_origin = s.taboption( this_tab, form.Value, 'allow_origin', _('Allow origin'), _('Set Access-Control-Allow-Origin header.') + ' ' + _('Default: disabled')); allow_origin.datatype = 'string'; allow_origin.optional = true; const instance_id = s.taboption( this_tab, form.Value, 'instance_id', _('Instance ID'), _('A short string identifier to be displayed in the /state handle.') + '
    ' + _('It must satisfy regexp') + ' ^[a-zA-Z0-9./+_-]*$.' + ' ' + _('Default: an empty string')); instance_id.datatype = 'string'; instance_id.optional = true; const server_timeout = s.taboption( this_tab, form.Value, 'server_timeout', _('Server timeout'), _('Timeout for client connections. Default: 10 seconds')); server_timeout.datatype = 'and(uinteger, range(0, 60))'; server_timeout.placeholder = '10'; server_timeout.optional = true; // JPEG sink this_tab = 'sink_jpeg'; const jpeg_sink = s.taboption( this_tab, form.Value, 'jpeg_sink', _('JPEG sink'), _('Use the shared memory to sink JPEG frames.') + '
    ' + _('The name should end with a suffix .jpg or .jpeg') + ' ' + _('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', _('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 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', _('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 on stop'), _('Remove shared memory on stop.') + ' ' + _('Default: disabled')); // RAW sink this_tab = 'sink_raw'; const raw_sink = s.taboption( this_tab, form.Value, 'raw_sink', _('RAW sink'), _('Use the shared memory to sink RAW frames.') + '
    ' + _('The name should end with a suffix .raw') + ' ' + _('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', _('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', _('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', _('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 on stop'), _('Remove shared memory on stop. Default: disabled')); // H264 sink this_tab = 'sink_h264'; const h264_sink = s.taboption( this_tab, form.Value, 'h264_sink', _('H264 sink'), _('Use the shared memory to sink H264 frames.') + '
    ' + _('The name should end with a suffix .h264') + ' ' + _('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', _('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_client_ttl = s.taboption( this_tab, form.Value, 'h264_sink_client_ttl', _('Client TTL'), _('Default: 10 seconds')); 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', _('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_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', _('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', _('M2M device'), _('Path to V4L2 M2M encoder device. Default: auto select')); h264_m2m_device.root_directory = '/dev'; 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'; // logging 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)') + '
    ' + _('Enabling debugging messages can slow down the program') + '
    ' + _('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') + '
    ' + _('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', _('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')); brightness.validate = validate_int_default_auto; brightness.placeholder = '128 | default | auto'; brightness.optional = true; 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'), _('number | default. Blank: no change')); saturation.validate = validate_int_default; saturation.placeholder = '128 | default'; saturation.optional = true; const gamma = s.taboption( this_tab, form.Value, 'gamma', _('Gamma'), _('number | default. Blank: no change')); gamma.validate = validate_int_default; gamma.placeholder = 'default'; gamma.optional = true; 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 color_effect = s.taboption( this_tab, form.Value, 'color_effect', _('Colour effect'), _('number | default. Blank: no change')); 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 backlight_compensation = s.taboption( this_tab, form.Value, 'backlight_compensation', _('Backlight compensation'), _('number | default. Blank: no change')); backlight_compensation.validate = validate_int_default; backlight_compensation.placeholder = '0 | default'; backlight_compensation.optional = true; const flip_horizontal = s.taboption( this_tab, form.Value, 'flip_horizontal', _('Flip horizontal'), _('number | default. Blank: no change')); 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; return m.render(); }, });