ustreamer: add package 6.51
authorGeorgi Valkov <redacted>
Sun, 1 Feb 2026 01:07:13 +0000 (03:07 +0200)
committerHannu Nyman <redacted>
Sat, 7 Feb 2026 10:25:12 +0000 (12:25 +0200)
With mjpg-streamer pending removal [1], it would be nice if we add
a compatible replacement which is under active development.

ustreamer offers a better performance, especially when the
camera supports MJPEG encoding in hardware.

The package already includes OpenWRT support files ./pkg/openwrt
but they needed heavy editing, so it is more efficient to copy
the scripts and configuration, instead of using patches.

Notable changes:
While the init.d script can run in the background when no camera is
connected, it is more efficient to indicate no active instances.
A hotplug script is introduced to start and stop the service when
cameras are added or removed.

If the configured format or encoding are unsupported, a compatible
alternative is automatically selected, so I changed the default
configuration to use MJPEG encoding in hardware for better performance.

HACKS:
MAKE_FLAGS += WITH_SETPROCTITLE=0
is added to workaround the following linker error:
undefined reference to setproctitle_init

This symbol is defined in libbsd, however adding the build dependency
does not resolve the error, because -lbsd is added conditionally, only
when uname -s contains linux. This is unreliable and fails when
cross-compiling on a macOS host. An upstream fix is needed.

An alternative is to use
PKG_UNPACK=$(HOST_TAR) -C $(PKG_BUILD_DIR) --strip=2 -xf $(DL_DIR)/$(PKG_SOURCE)
however this modifies the directory structure, so patches would need
path editing to maintain upstream compatibility.

TODO:
luci-app-mjpg-streamer which is also pending removal [2] is able to
open the HTTP stream from ustreamer. It would be nice to create
luci-app-ustreamer based on that.

[1] https://github.com/openwrt/packages/pull/28344
[2] https://github.com/openwrt/luci/pull/8221

Signed-off-by: Georgi Valkov <redacted>
multimedia/ustreamer/Makefile [new file with mode: 0644]
multimedia/ustreamer/files/ustreamer.config [new file with mode: 0644]
multimedia/ustreamer/files/ustreamer.hotplug [new file with mode: 0644]
multimedia/ustreamer/files/ustreamer.init [new file with mode: 0644]

diff --git a/multimedia/ustreamer/Makefile b/multimedia/ustreamer/Makefile
new file mode 100644 (file)
index 0000000..7ce259b
--- /dev/null
@@ -0,0 +1,53 @@
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=ustreamer
+PKG_VERSION:=6.51
+PKG_RELEASE:=1
+PKG_MAINTAINER:=Georgi Valkov <gvalkov@gmail.com>
+
+PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
+PKG_SOURCE_URL:=https://codeload.github.com/pikvm/ustreamer/tar.gz/v$(PKG_VERSION)?
+PKG_HASH:=384e90b0b8e9669cf903fad10e361401bfb1b2f808be44498e3855a65d7c0145
+
+PKG_LICENSE:=GPL-3.0
+PKG_LICENSE_FILES:=LICENSE
+
+include $(INCLUDE_DIR)/package.mk
+
+MAKE_FLAGS += WITH_SETPROCTITLE=0
+
+define Package/ustreamer
+       SECTION:=multimedia
+       CATEGORY:=Multimedia
+       TITLE:=Lightweight and fast MJPEG-HTTP streamer
+       DEPENDS:=+libatomic +libjpeg +libevent2 +libevent2-pthreads
+       URL:=https://github.com/pikvm/ustreamer
+endef
+
+define Package/ustreamer/description
+uStreamer is a lightweight and very quick server to stream MJPEG video
+from any V4L2 device to the net. All new browsers have native support
+of this video format, as well as most video players such as mplayer,
+VLC etc. uStreamer is a part of the PiKVM project designed to stream
+VGA and HDMI screencast hardware data with the highest resolution and
+FPS possible.
+endef
+
+define Package/ustreamer/install
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/ustreamer $(1)/usr/bin/ustreamer
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/ustreamer-dump $(1)/usr/bin/ustreamer-dump
+       $(INSTALL_DIR) $(1)/etc/config
+       $(INSTALL_CONF) ./files/ustreamer.config $(1)/etc/config/ustreamer
+       $(INSTALL_DIR) $(1)/etc/init.d
+       $(INSTALL_BIN) ./files/ustreamer.init $(1)/etc/init.d/ustreamer
+       $(INSTALL_DIR) $(1)/etc/hotplug.d/usb
+       $(INSTALL_DATA) ./files/ustreamer.hotplug $(1)/etc/hotplug.d/usb/20-ustreamer
+endef
+
+$(eval $(call BuildPackage,ustreamer))
diff --git a/multimedia/ustreamer/files/ustreamer.config b/multimedia/ustreamer/files/ustreamer.config
new file mode 100644 (file)
index 0000000..1861499
--- /dev/null
@@ -0,0 +1,99 @@
+
+config ustreamer 'video0'
+       option enabled '1'
+
+       option device '/dev/video0'
+       option device_timeout '5'
+       option input '0'
+
+#      option resolution '1920x1080'
+#      option resolution '1280x720'
+#      option resolution '800x600'
+#      option resolution '864x480'
+       option resolution '640x480'
+       option desired_fps '0'
+       option slowdown '1'
+       option format 'MJPEG'
+#      option format 'YUYV'
+#      option encoder 'CPU'
+       option encoder 'HW'
+       option quality '80'
+
+       option host '::'
+       option port '8080'
+       option user ''
+       option pass ''
+
+       # capturing
+#      option allow_truncated_frames '0'
+#      option format_swap_rgb '0'
+#      option persistent '0'
+#      option dv_timings '0'
+#      option tv_standard 'PAL'
+#      option tv_standard 'NTSC'
+#      option tv_standard 'SECAM'
+#      option io_method 'MMAP'
+#      option io_method 'USERPTR'
+#      option buffers '3'
+#      option workers '2'
+#      option m2m_device '/dev/path'
+#      option min_frame_size '128'
+#      option device_error_delay '1'
+
+       # HTTP server
+#      option tcp_nodelay '1'
+#      option unix_rm '0'
+#      option static '/www/webcam'
+#      option unix '/path/to/socket'
+#      option unix_mode '660'
+#      option drop_same_frames '0'
+#      option fake_resolution '640x480'
+#      option allow_origin ''
+#      option instance_id ''
+#      option server_timeout '10'
+
+       # JPEG sink
+#      option jpeg_sink 'name.jpeg'
+#      option jpeg_sink_mode '660'
+#      option jpeg_sink_client_ttl '10'
+#      option jpeg_sink_timeout '1'
+#      option jpeg_sink_rm '0'
+
+       # RAW sink
+#      option raw_sink 'name.raw'
+#      option raw_sink_mode '660'
+#      option raw_sink_client_ttl '10'
+#      option raw_sink_timeout '1'
+#      option raw_sink_rm '0'
+
+       # H264 sink
+#      option h264_sink 'name.h264'
+#      option h264_sink_mode '660'
+#      option h264_sink_client_ttl '10'
+#      option h264_sink_timeout '1'
+#      option h264_sink_rm '0'
+#      option h264_boost '0'
+#      option h264_bitrate '5000'
+#      option h264_gop '30'
+#      option h264_m2m_device '/dev/path'
+#      option exit_on_no_clients '0'
+
+       # logging
+#      option log_level '0'
+
+       # image control
+#      option image_default '0'
+#      option brightness '128'
+#      option contrast '128'
+#      option saturation '128'
+#      option gamma ''
+#      option gain 'auto'
+#      option hue ''
+#      option sharpness '128'
+#      option color_effect ''
+#      option white_balance 'auto'
+#      option white_balance '2000-6500'
+#      option backlight_compensation '0'
+#      option flip_horizontal '0'
+#      option flip_vertical '0'
+#      option rotate ''
diff --git a/multimedia/ustreamer/files/ustreamer.hotplug b/multimedia/ustreamer/files/ustreamer.hotplug
new file mode 100644 (file)
index 0000000..6091761
--- /dev/null
@@ -0,0 +1,10 @@
+case "$ACTION" in
+       add)
+               # start process
+               ls /dev/video* > /dev/null && /etc/init.d/ustreamer start
+               ;;
+       remove)
+               # stop process
+               ls /dev/video* > /dev/null || /etc/init.d/ustreamer stop
+               ;;
+esac
diff --git a/multimedia/ustreamer/files/ustreamer.init b/multimedia/ustreamer/files/ustreamer.init
new file mode 100644 (file)
index 0000000..2be2dac
--- /dev/null
@@ -0,0 +1,289 @@
+#!/bin/sh /etc/rc.common
+# Copyright (C) 2009-2026 OpenWrt.org
+
+START=90
+STOP=10
+
+USE_PROCD=1
+PROG=/usr/bin/ustreamer
+
+start_instance() {
+       local enabled=0
+       local device="/dev/video0"
+
+       config_get_bool enabled "$1" 'enabled' "$enabled"
+       [ "$enabled" -eq 0 ] && return
+
+       config_get device "$1" 'device' "$device"
+       [ -n "$device" -a -c "$device" ] || return
+
+       # primary
+       local input=""
+       local format="YUYV"
+       local encoder="CPU"
+       local quality=80
+       local resolution="640x480"
+       local desired_fps=0
+       local slowdown=1
+       local device_timeout=5
+       local host="::"
+       local port=8080
+       local user=""
+       local pass=""
+
+       # capturing
+       local allow_truncated_frames=0
+       local format_swap_rgb=0
+       local persistent=0
+       local dv_timings=0
+       local tv_standard=""
+       local io_method=""
+       local buffers=""
+       local workers=""
+       local m2m_device=""
+       local min_frame_size=""
+       local device_error_delay=""
+
+       # HTTP server
+       local tcp_nodelay=0
+       local unix_rm=0
+       local static=""
+       local unix=""
+       local unix_mode=""
+       local drop_same_frames=""
+       local fake_resolution=""
+       local allow_origin=""
+       local instance_id=""
+       local server_timeout=""
+
+       # JPEG sink
+       local jpeg_sink=""
+       local jpeg_sink_mode=""
+       local jpeg_sink_client_ttl=""
+       local jpeg_sink_timeout=""
+       local jpeg_sink_rm=0
+
+       # RAW sink
+       local raw_sink=""
+       local raw_sink_mode=""
+       local raw_sink_client_ttl=""
+       local raw_sink_timeout=""
+       local raw_sink_rm=0
+
+       # H264 sink
+       local h264_sink=""
+       local h264_sink_mode=""
+       local h264_sink_client_ttl=""
+       local h264_sink_timeout=""
+       local h264_sink_rm=0
+       local h264_boost=0
+       local h264_bitrate=""
+       local h264_gop=""
+       local h264_m2m_device=""
+       local exit_on_no_clients=""
+
+       # logging
+       local log_level=""
+
+       # image control
+       local image_default=0
+       local brightness=""
+       local contrast=""
+       local saturation=""
+       local gamma=""
+       local gain=""
+       local hue=""
+       local sharpness=""
+       local color_effect=""
+       local white_balance=""
+       local backlight_compensation=""
+       local flip_horizontal=""
+       local flip_vertical=""
+       local rotate=""
+
+       # primary
+       config_get input "$1" 'input' "$input"
+       config_get format "$1" 'format' "$format"
+       config_get encoder "$1" 'encoder' "$encoder"
+       config_get quality "$1" 'quality' "$quality"
+       config_get resolution "$1" 'resolution' "$resolution"
+       config_get desired_fps "$1" 'desired_fps' "$desired_fps"
+       config_get_bool slowdown "$1" 'slowdown' "$slowdown"
+       config_get device_timeout "$1" 'device_timeout' "$device_timeout"
+       config_get host "$1" 'host' "$host"
+       config_get port "$1" 'port' "$port"
+       config_get user "$1" 'user' "$user"
+       config_get pass "$1" 'pass' "$pass"
+
+       # capturing
+       config_get_bool allow_truncated_frames "$1" 'allow_truncated_frames' "$allow_truncated_frames"
+       config_get_bool format_swap_rgb "$1" 'format_swap_rgb' "$format_swap_rgb"
+       config_get_bool persistent "$1" 'persistent' "$persistent"
+       config_get_bool dv_timings "$1" 'dv_timings' "$dv_timings"
+       config_get tv_standard "$1" 'tv_standard' "$tv_standard"
+       config_get io_method "$1" 'io_method' "$io_method"
+       config_get buffers "$1" 'buffers' "$buffers"
+       config_get workers "$1" 'workers' "$workers"
+       config_get m2m_device "$1" 'm2m_device' "$m2m_device"
+       config_get min_frame_size "$1" 'min_frame_size' "$min_frame_size"
+       config_get device_error_delay "$1" 'device_error_delay' "$device_error_delay"
+
+       # HTTP server
+       config_get_bool tcp_nodelay "$1" 'tcp_nodelay' "$tcp_nodelay"
+       config_get_bool unix_rm "$1" 'unix_rm' "$unix_rm"
+       config_get static "$1" 'static' "$static"
+       config_get unix "$1" 'unix' "$unix"
+       config_get unix_mode "$1" 'unix_mode' "$unix_mode"
+       config_get drop_same_frames "$1" 'drop_same_frames' "$drop_same_frames"
+       config_get fake_resolution "$1" 'fake_resolution' "$fake_resolution"
+       config_get allow_origin "$1" 'allow_origin' "$allow_origin"
+       config_get instance_id "$1" 'instance_id' "$instance_id"
+       config_get server_timeout "$1" 'server_timeout' "$server_timeout"
+
+       # JPEG sink
+       config_get jpeg_sink "$1" 'jpeg_sink' "$jpeg_sink"
+       config_get jpeg_sink_mode "$1" 'jpeg_sink_mode' "$jpeg_sink_mode"
+       config_get jpeg_sink_client_ttl "$1" 'jpeg_sink_client_ttl' "$jpeg_sink_client_ttl"
+       config_get jpeg_sink_timeout "$1" 'jpeg_sink_timeout' "$jpeg_sink_timeout"
+       config_get_bool jpeg_sink_rm "$1" 'jpeg_sink_rm' "$jpeg_sink_rm"
+
+       # RAW sink
+       config_get raw_sink "$1" 'raw_sink' "$raw_sink"
+       config_get raw_sink_mode "$1" 'raw_sink_mode' "$raw_sink_mode"
+       config_get raw_sink_client_ttl "$1" 'raw_sink_client_ttl' "$raw_sink_client_ttl"
+       config_get raw_sink_timeout "$1" 'raw_sink_timeout' "$raw_sink_timeout"
+       config_get_bool raw_sink_rm "$1" 'raw_sink_rm' "$raw_sink_rm"
+
+       # H264 sink
+       config_get h264_sink "$1" 'h264_sink' "$h264_sink"
+       config_get h264_sink_mode "$1" 'h264_sink_mode' "$h264_sink_mode"
+       config_get h264_sink_client_ttl "$1" 'h264_sink_client_ttl' "$h264_sink_client_ttl"
+       config_get h264_sink_timeout "$1" 'h264_sink_timeout' "$h264_sink_timeout"
+       config_get_bool h264_sink_rm "$1" 'h264_sink_rm' "$h264_sink_rm"
+       config_get_bool h264_boost "$1" 'h264_boost' "$h264_boost"
+       config_get h264_bitrate "$1" 'h264_bitrate' "$h264_bitrate"
+       config_get h264_gop "$1" 'h264_gop' "$h264_gop"
+       config_get h264_m2m_device "$1" 'h264_m2m_device' "$h264_m2m_device"
+       config_get exit_on_no_clients "$1" 'exit_on_no_clients' "$exit_on_no_clients"
+
+       # logging
+       config_get log_level "$1" 'log_level' "$log_level"
+
+       # image control
+       config_get_bool image_default "$1" 'image_default' "$image_default"
+       config_get brightness "$1" 'brightness' "$brightness"
+       config_get contrast "$1" 'contrast' "$contrast"
+       config_get saturation "$1" 'saturation' "$saturation"
+       config_get gamma "$1" 'gamma' "$gamma"
+       config_get gain "$1" 'gain' "$gain"
+       config_get hue "$1" 'hue' "$hue"
+       config_get sharpness "$1" 'sharpness' "$sharpness"
+       config_get color_effect "$1" 'color_effect' "$color_effect"
+       config_get white_balance "$1" 'white_balance' "$white_balance"
+       config_get backlight_compensation "$1" 'backlight_compensation' "$backlight_compensation"
+       config_get flip_horizontal "$1" 'flip_horizontal' "$flip_horizontal"
+       config_get flip_vertical "$1" 'flip_vertical' "$flip_vertical"
+       config_get rotate "$1" 'rotate' "$rotate"
+
+       # configure service
+       procd_open_instance
+       procd_set_param command "$PROG"
+
+       # primary
+       [ -n "$device" ] && procd_append_param command --device "$device"
+       [ -n "$input" ] && procd_append_param command --input "$input"
+       [ -n "$format" ] && procd_append_param command --format "$format"
+       [ -n "$encoder" ] && procd_append_param command --encoder "$encoder"
+       [ -n "$quality" ] && procd_append_param command --quality "$quality"
+       [ -n "$resolution" ] && procd_append_param command --resolution "$resolution"
+       [ -n "$desired_fps" ] && procd_append_param command --desired-fps "$desired_fps"
+       [ "$slowdown" -ne "0" ] && procd_append_param command --slowdown
+       [ -n "$device_timeout" ] && procd_append_param command --device-timeout "$device_timeout"
+       [ -n "$host" ] && procd_append_param command --host "$host"
+       [ -n "$port" ] && procd_append_param command --port "$port"
+       [ -n "$user" ] && procd_append_param command --user "$user"
+       [ -n "$pass" ] && procd_append_param command --passwd "$pass"
+
+       # capturing
+       [ "$allow_truncated_frames" -ne "0" ] && procd_append_param command --allow-truncated-frames
+       [ "$format_swap_rgb" -ne "0" ] && procd_append_param command --format-swap-rgb
+       [ "$persistent" -ne "0" ] && procd_append_param command --persistent
+       [ "$dv_timings" -ne "0" ] && procd_append_param command --dv-timings
+       [ -n "$tv_standard" ] && procd_append_param command --tv-standard "$tv_standard"
+       [ -n "$io_method" ] && procd_append_param command --io-method "$io_method"
+       [ -n "$buffers" ] && procd_append_param command --buffers "$buffers"
+       [ -n "$workers" ] && procd_append_param command --workers "$workers"
+       [ -n "$m2m_device" ] && procd_append_param command --m2m-device "$m2m_device"
+       [ -n "$min_frame_size" ] && procd_append_param command --min-frame-size "$min_frame_size"
+       [ -n "$device_error_delay" ] && procd_append_param command --device-error-delay "$device_error_delay"
+
+       # HTTP server
+       [ "$tcp_nodelay" -ne "0" ] && procd_append_param command --tcp-nodelay
+       [ "$unix_rm" -ne "0" ] && procd_append_param command --unix-rm
+       [ -n "$static" ] && procd_append_param command --static "$static"
+       [ -n "$unix" ] && procd_append_param command --unix "$unix"
+       [ -n "$unix_mode" ] && procd_append_param command --unix-mode "$unix_mode"
+       [ -n "$drop_same_frames" ] && procd_append_param command --drop-same-frames "$drop_same_frames"
+       [ -n "$fake_resolution" ] && procd_append_param command --fake-resolution "$fake_resolution"
+       [ -n "$allow_origin" ] && procd_append_param command --allow-origin "$allow_origin"
+       [ -n "$instance_id" ] && procd_append_param command --instance-id "$instance_id"
+       [ -n "$server_timeout" ] && procd_append_param command --server-timeout "$server_timeout"
+
+       # JPEG sink
+       [ -n "$jpeg_sink" ] && procd_append_param command --jpeg-sink "$jpeg_sink"
+       [ -n "$jpeg_sink_mode" ] && procd_append_param command --jpeg-sink-mode "$jpeg_sink_mode"
+       [ -n "$jpeg_sink_client_ttl" ] && procd_append_param command --jpeg-sink-client-ttl "$jpeg_sink_client_ttl"
+       [ -n "$jpeg_sink_timeout" ] && procd_append_param command --jpeg-sink-timeout "$jpeg_sink_timeout"
+       [ "$jpeg_sink_rm" -ne "0" ] && procd_append_param command --jpeg-sink-rm
+
+       # RAW sink
+       [ -n "$raw_sink" ] && procd_append_param command --raw-sink "$raw_sink"
+       [ -n "$raw_sink_mode" ] && procd_append_param command --raw-sink-mode "$raw_sink_mode"
+       [ -n "$raw_sink_client_ttl" ] && procd_append_param command --raw-sink-client-ttl "$raw_sink_client_ttl"
+       [ -n "$raw_sink_timeout" ] && procd_append_param command --raw-sink-timeout "$raw_sink_timeout"
+       [ "$raw_sink_rm" -ne "0" ] && procd_append_param command --raw-sink-rm
+
+       # H264 sink
+       [ -n "$h264_sink" ] && procd_append_param command --h264-sink "$h264_sink"
+       [ -n "$h264_sink_mode" ] && procd_append_param command --h264-sink-mode "$h264_sink_mode"
+       [ -n "$h264_sink_client_ttl" ] && procd_append_param command --h264-sink-client-ttl "$h264_sink_client_ttl"
+       [ -n "$h264_sink_timeout" ] && procd_append_param command --h264-sink-timeout "$h264_sink_timeout"
+       [ "$h264_sink_rm" -ne "0" ] && procd_append_param command --h264-sink-rm
+       [ "$h264_boost" -ne "0" ] && procd_append_param command --h264-boost
+       [ -n "$h264_bitrate" ] && procd_append_param command --h264-bitrate "$h264_bitrate"
+       [ -n "$h264_gop" ] && procd_append_param command --h264-gop "$h264_gop"
+       [ -n "$h264_m2m_device" ] && procd_append_param command --h264-m2m-device "$h264_m2m_device"
+       [ -n "$exit_on_no_clients" ] && procd_append_param command --exit-on-no-clients "$exit_on_no_clients"
+
+       # logging
+       [ -n "$log_level" ] && procd_append_param command --log-level "$log_level"
+
+       # image control
+       [ "$image_default" -ne "0" ] && procd_append_param command --image-default
+       [ -n "$brightness" ] && procd_append_param command --brightness "$brightness"
+       [ -n "$contrast" ] && procd_append_param command --contrast "$contrast"
+       [ -n "$saturation" ] && procd_append_param command --saturation "$saturation"
+       [ -n "$gamma" ] && procd_append_param command --gamma "$gamma"
+       [ -n "$gain" ] && procd_append_param command --gain "$gain"
+       [ -n "$hue" ] && procd_append_param command --hue "$hue"
+       [ -n "$sharpness" ] && procd_append_param command --sharpness "$sharpness"
+       [ -n "$color_effect" ] && procd_append_param command --color-effect "$color_effect"
+       [ -n "$white_balance" ] && procd_append_param command --white-balance "$white_balance"
+       [ -n "$backlight_compensation" ] && procd_append_param command --backlight-compensation "$backlight_compensation"
+       [ -n "$flip_horizontal" ] && procd_append_param command --flip-horizontal "$flip_horizontal"
+       [ -n "$flip_vertical" ] && procd_append_param command --flip-vertical "$flip_vertical"
+       [ -n "$rotate" ] && procd_append_param command --rotate "$rotate"
+
+       procd_add_mdns http tcp "$port" "daemon=ustreamer"
+       procd_set_param respawn 3600 5 5
+       procd_close_instance
+}
+
+start_service() {
+       config_load 'ustreamer'
+       config_foreach start_instance 'ustreamer'
+}
+
+service_triggers() {
+       procd_add_reload_trigger 'ustreamer'
+}
git clone https://git.99rst.org/PROJECT