uvc: add MJPEG webcam path

This commit is contained in:
Brad Stein 2026-01-06 21:06:20 -03:00
parent dccc6601b1
commit 6295720b96
5 changed files with 150 additions and 67 deletions

View File

@ -31,20 +31,28 @@ impl CameraCapture {
None => "/dev/video0".into(),
};
let use_mjpg = std::env::var("LESAVKA_CAM_MJPG").is_ok()
let use_mjpg_source = std::env::var("LESAVKA_CAM_MJPG").is_ok()
|| std::env::var("LESAVKA_CAM_FORMAT")
.ok()
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "mjpg" | "mjpeg" | "jpeg"))
.unwrap_or(false);
let (enc, kf_prop, kf_val) = if use_mjpg {
let output_mjpeg = std::env::var("LESAVKA_CAM_CODEC")
.ok()
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "mjpeg" | "mjpg" | "jpeg"))
.unwrap_or(false);
let (enc, kf_prop, kf_val) = if use_mjpg_source && !output_mjpeg {
("x264enc", "key-int-max", "30")
} else {
Self::choose_encoder()
};
if use_mjpg {
if use_mjpg_source && !output_mjpeg {
tracing::info!("📸 using MJPG source with software encode");
}
tracing::info!("📸 using encoder element: {enc}");
if output_mjpeg {
tracing::info!("📸 outputting MJPEG frames for UVC");
} else {
tracing::info!("📸 using encoder element: {enc}");
}
let width = env_u32("LESAVKA_CAM_WIDTH", 1280);
let height = env_u32("LESAVKA_CAM_HEIGHT", 720);
let fps = env_u32("LESAVKA_CAM_FPS", 25).max(1);
@ -82,7 +90,24 @@ impl CameraCapture {
// * nvh264enc needs NVMM memory caps;
// * vaapih264enc wants system-memory caps;
// * x264enc needs the usual raw caps.
let desc = if use_mjpg {
let desc = if output_mjpeg {
if use_mjpg_source {
format!(
"v4l2src device={dev} do-timestamp=true ! \
image/jpeg,width={width},height={height} ! \
queue max-size-buffers=30 leaky=downstream ! \
appsink name=asink emit-signals=true max-buffers=60 drop=true"
)
} else {
format!(
"v4l2src device={dev} do-timestamp=true ! \
video/x-raw,width={width},height={height},framerate={fps}/1 ! \
videoconvert ! jpegenc ! \
queue max-size-buffers=30 leaky=downstream ! \
appsink name=asink emit-signals=true max-buffers=60 drop=true"
)
}
} else if use_mjpg_source {
format!(
"v4l2src device={dev} do-timestamp=true ! \
image/jpeg,width={width},height={height} ! \

View File

@ -20,12 +20,16 @@ UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-720}
UVC_FPS=${LESAVKA_UVC_FPS:-25}
UVC_DISABLE_IRQ=${LESAVKA_UVC_DISABLE_IRQ:-}
UVC_BULK=${LESAVKA_UVC_BULK:-}
UVC_CODEC=${LESAVKA_UVC_CODEC:-yuyv}
if [[ -n ${LESAVKA_UVC_MJPEG:-} ]]; then
UVC_CODEC=mjpeg
fi
MAX_SPEED=${LESAVKA_MAX_SPEED:-high-speed}
if [[ -z $UVC_INTERVAL ]]; then
UVC_INTERVAL=$((10000000 / UVC_FPS))
fi
UVC_FRAME_SIZE=$((UVC_WIDTH * UVC_HEIGHT * 2))
UVC_FRAME_SIZE=${LESAVKA_UVC_FRAME_SIZE:-$((UVC_WIDTH * UVC_HEIGHT * 2))}
wait_for_enum() {
local tries=${1:-50} # 50 x 100ms = 5s
@ -171,28 +175,55 @@ if [[ -z $DISABLE_UVC ]]; then
echo 1 >"$F/streaming_bulk" 2>/dev/null || true
fi
# ── 1. FORMAT DESCRIPTOR (uncompressed YUY2, 16 bpp) ──────────────
mkdir -p "$F/streaming/uncompressed/yuyv"
# GUID = {59555932-0000-0010-8000-00aa00389b71} (“YUY2”) little-endian
printf '\x59\x55\x59\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' \
>"$F/streaming/uncompressed/yuyv/guidFormat"
echo 16 >"$F/streaming/uncompressed/yuyv/bBitsPerPixel"
# ── 1. FORMAT DESCRIPTOR ──────────────────────────────────────────
if [[ "$UVC_CODEC" == "mjpeg" ]]; then
mkdir -p "$F/streaming/mjpeg/m"
echo 1 >"$F/streaming/mjpeg/m/bFormatIndex"
echo 1 >"$F/streaming/mjpeg/m/bDefaultFrameIndex"
echo 0 >"$F/streaming/mjpeg/m/bAspectRatioX"
echo 0 >"$F/streaming/mjpeg/m/bAspectRatioY"
echo 0 >"$F/streaming/mjpeg/m/bmInterlaceFlags"
echo 0 >"$F/streaming/mjpeg/m/bmFlags"
echo 0 >"$F/streaming/mjpeg/m/bmaControls"
# ── 2. FRAME DESCRIPTOR (480p @ 30 fps) ───────────────────────────
mkdir -p "$F/streaming/uncompressed/yuyv/480p"
echo "$UVC_WIDTH" >"$F/streaming/uncompressed/yuyv/480p/wWidth"
echo "$UVC_HEIGHT" >"$F/streaming/uncompressed/yuyv/480p/wHeight"
echo "$UVC_FRAME_SIZE" >"$F/streaming/uncompressed/yuyv/480p/dwMaxVideoFrameBufferSize"
echo "$UVC_INTERVAL" >"$F/streaming/uncompressed/yuyv/480p/dwDefaultFrameInterval"
cat <<EOF >"$F/streaming/uncompressed/yuyv/480p/dwFrameInterval"
mkdir -p "$F/streaming/mjpeg/m/720p"
echo 1 >"$F/streaming/mjpeg/m/720p/bFrameIndex"
echo 0 >"$F/streaming/mjpeg/m/720p/bmCapabilities"
echo "$UVC_WIDTH" >"$F/streaming/mjpeg/m/720p/wWidth"
echo "$UVC_HEIGHT" >"$F/streaming/mjpeg/m/720p/wHeight"
echo "$UVC_FRAME_SIZE" >"$F/streaming/mjpeg/m/720p/dwMaxVideoFrameBufferSize"
echo "$UVC_INTERVAL" >"$F/streaming/mjpeg/m/720p/dwDefaultFrameInterval"
cat <<EOF >"$F/streaming/mjpeg/m/720p/dwFrameInterval"
${UVC_INTERVAL}
$((UVC_INTERVAL * 2))
EOF
else
# uncompressed YUY2, 16 bpp
mkdir -p "$F/streaming/uncompressed/yuyv"
# GUID = {59555932-0000-0010-8000-00aa00389b71} (“YUY2”) little-endian
printf '\x59\x55\x59\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' \
>"$F/streaming/uncompressed/yuyv/guidFormat"
echo 16 >"$F/streaming/uncompressed/yuyv/bBitsPerPixel"
mkdir -p "$F/streaming/uncompressed/yuyv/480p"
echo "$UVC_WIDTH" >"$F/streaming/uncompressed/yuyv/480p/wWidth"
echo "$UVC_HEIGHT" >"$F/streaming/uncompressed/yuyv/480p/wHeight"
echo "$UVC_FRAME_SIZE" >"$F/streaming/uncompressed/yuyv/480p/dwMaxVideoFrameBufferSize"
echo "$UVC_INTERVAL" >"$F/streaming/uncompressed/yuyv/480p/dwDefaultFrameInterval"
cat <<EOF >"$F/streaming/uncompressed/yuyv/480p/dwFrameInterval"
${UVC_INTERVAL}
$((UVC_INTERVAL * 2))
EOF
fi
# ── 3. REQUIRED HEADER LINKS (per UVC gadget docs) ────────────────
mkdir -p "$F/streaming/header/h"
pushd "$F/streaming/header/h" >/dev/null
ln -s ../../uncompressed/yuyv yuyv
if [[ "$UVC_CODEC" == "mjpeg" ]]; then
ln -s ../../mjpeg/m mjpeg
else
ln -s ../../uncompressed/yuyv yuyv
fi
popd >/dev/null
for s in fs hs ss; do

View File

@ -150,6 +150,7 @@ Type=oneshot
ExecStart=/usr/local/bin/lesavka-core.sh
RemainAfterExit=yes
Environment=LESAVKA_UVC_FALLBACK=0
Environment=LESAVKA_UVC_CODEC=mjpeg
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE
AmbientCapabilities=CAP_SYS_MODULE
MountFlags=slave
@ -170,6 +171,7 @@ Restart=always
Environment=RUST_LOG=lesavka_server=info,lesavka_server::audio=info,lesavka_server::video=debug,lesavka_server::gadget=info
Environment=RUST_BACKTRACE=1
Environment=GST_DEBUG="*:2,alsasink:6,alsasrc:6"
Environment=LESAVKA_UVC_CODEC=mjpeg
Restart=always
RestartSec=5
StandardError=append:/tmp/lesavka-server.stderr

View File

@ -88,6 +88,7 @@ struct UvcConfig {
fps: u32,
interval: u32,
max_packet: u32,
frame_size: u32,
}
struct UvcState {
@ -251,6 +252,7 @@ impl UvcConfig {
let fps = env_u32("LESAVKA_UVC_FPS", 25).max(1);
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
let frame_size = env_u32("LESAVKA_UVC_FRAME_SIZE", width * height * 2);
if env::var("LESAVKA_UVC_BULK").is_ok() {
max_packet = max_packet.min(512);
} else {
@ -269,6 +271,7 @@ impl UvcConfig {
fps,
interval,
max_packet,
frame_size,
}
}
}
@ -626,7 +629,6 @@ fn send_stall(fd: i32, req: libc::c_ulong) -> Result<()> {
fn build_streaming_control(cfg: &UvcConfig, ctrl_len: usize) -> [u8; STREAM_CTRL_SIZE_MAX] {
let mut buf = [0u8; STREAM_CTRL_SIZE_MAX];
let frame_size = cfg.width * cfg.height * 2;
write_le16(&mut buf[0..2], 1); // bmHint: dwFrameInterval
buf[2] = 1; // bFormatIndex
@ -637,7 +639,7 @@ fn build_streaming_control(cfg: &UvcConfig, ctrl_len: usize) -> [u8; STREAM_CTRL
write_le16(&mut buf[12..14], 0);
write_le16(&mut buf[14..16], 0);
write_le16(&mut buf[16..18], 0);
write_le32(&mut buf[18..22], frame_size);
write_le32(&mut buf[18..22], cfg.frame_size);
write_le32(&mut buf[22..26], cfg.max_packet);
if ctrl_len >= STREAM_CTRL_SIZE_15 {
write_le32(&mut buf[26..30], 48_000_000);

View File

@ -300,17 +300,11 @@ impl WebcamSink {
let width = env_u32("LESAVKA_UVC_WIDTH", 1280) as i32;
let height = env_u32("LESAVKA_UVC_HEIGHT", 720) as i32;
let fps = env_u32("LESAVKA_UVC_FPS", 25).max(1) as i32;
let caps_h264 = gst::Caps::builder("video/x-h264")
.field("stream-format", "byte-stream")
.field("alignment", "au")
.build();
let raw_caps = gst::Caps::builder("video/x-raw")
.field("format", "YUY2")
.field("width", width)
.field("height", height)
.field("framerate", gst::Fraction::new(fps, 1))
.build();
let use_mjpeg = std::env::var("LESAVKA_UVC_MJPEG").is_ok()
|| std::env::var("LESAVKA_UVC_CODEC")
.ok()
.map(|v| matches!(v.to_ascii_lowercase().as_str(), "mjpeg" | "mjpg" | "jpeg"))
.unwrap_or(false);
let src = gst::ElementFactory::make("appsrc")
.build()?
@ -318,43 +312,72 @@ impl WebcamSink {
.expect("appsrc");
src.set_is_live(true);
src.set_format(gst::Format::Time);
src.set_caps(Some(&caps_h264));
src.set_property("block", &true);
let h264parse = gst::ElementFactory::make("h264parse").build()?;
let decoder_name = Self::pick_decoder();
let decoder = gst::ElementFactory::make(decoder_name)
.build()
.with_context(|| format!("building decoder element {decoder_name}"))?;
let convert = gst::ElementFactory::make("videoconvert").build()?;
let scale = gst::ElementFactory::make("videoscale").build()?;
let caps = gst::ElementFactory::make("capsfilter")
.property("caps", &raw_caps)
.build()?;
let sink = gst::ElementFactory::make("v4l2sink")
.property("device", &uvc_dev)
.property("sync", &false)
.build()?;
if use_mjpeg {
let caps_mjpeg = gst::Caps::builder("image/jpeg")
.field("width", width)
.field("height", height)
.field("framerate", gst::Fraction::new(fps, 1))
.build();
src.set_caps(Some(&caps_mjpeg));
// Upcast to &gst::Element for the collection macros
pipeline.add_many(&[
src.upcast_ref(),
&h264parse,
&decoder,
&convert,
&scale,
&caps,
&sink,
])?;
gst::Element::link_many(&[
src.upcast_ref(),
&h264parse,
&decoder,
&convert,
&scale,
&caps,
&sink,
])?;
let jpegparse = gst::ElementFactory::make("jpegparse").build()?;
let queue = gst::ElementFactory::make("queue").build()?;
let sink = gst::ElementFactory::make("v4l2sink")
.property("device", &uvc_dev)
.property("sync", &false)
.build()?;
pipeline.add_many(&[src.upcast_ref(), &jpegparse, &queue, &sink])?;
gst::Element::link_many(&[src.upcast_ref(), &jpegparse, &queue, &sink])?;
} else {
let caps_h264 = gst::Caps::builder("video/x-h264")
.field("stream-format", "byte-stream")
.field("alignment", "au")
.build();
let raw_caps = gst::Caps::builder("video/x-raw")
.field("format", "YUY2")
.field("width", width)
.field("height", height)
.field("framerate", gst::Fraction::new(fps, 1))
.build();
src.set_caps(Some(&caps_h264));
let h264parse = gst::ElementFactory::make("h264parse").build()?;
let decoder_name = Self::pick_decoder();
let decoder = gst::ElementFactory::make(decoder_name)
.build()
.with_context(|| format!("building decoder element {decoder_name}"))?;
let convert = gst::ElementFactory::make("videoconvert").build()?;
let scale = gst::ElementFactory::make("videoscale").build()?;
let caps = gst::ElementFactory::make("capsfilter")
.property("caps", &raw_caps)
.build()?;
let sink = gst::ElementFactory::make("v4l2sink")
.property("device", &uvc_dev)
.property("sync", &false)
.build()?;
pipeline.add_many(&[
src.upcast_ref(),
&h264parse,
&decoder,
&convert,
&scale,
&caps,
&sink,
])?;
gst::Element::link_many(&[
src.upcast_ref(),
&h264parse,
&decoder,
&convert,
&scale,
&caps,
&sink,
])?;
}
pipeline.set_state(gst::State::Playing)?;
Ok(Self {