diff --git a/client/src/input/camera.rs b/client/src/input/camera.rs index fbe1d14..88b80a0 100644 --- a/client/src/input/camera.rs +++ b/client/src/input/camera.rs @@ -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} ! \ diff --git a/scripts/daemon/lesavka-core.sh b/scripts/daemon/lesavka-core.sh index bb46c6f..6b5f6c0 100644 --- a/scripts/daemon/lesavka-core.sh +++ b/scripts/daemon/lesavka-core.sh @@ -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 <"$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 <"$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 <"$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 diff --git a/scripts/install/server.sh b/scripts/install/server.sh index 6fabc0b..575d62e 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -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 diff --git a/server/src/bin/lesavka-uvc.rs b/server/src/bin/lesavka-uvc.rs index b51b8c1..09dad27 100644 --- a/server/src/bin/lesavka-uvc.rs +++ b/server/src/bin/lesavka-uvc.rs @@ -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); diff --git a/server/src/video.rs b/server/src/video.rs index d3a351b..95fb482 100644 --- a/server/src/video.rs +++ b/server/src/video.rs @@ -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)); - // Up‑cast 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 {