uvc: add MJPEG webcam path
This commit is contained in:
parent
dccc6601b1
commit
6295720b96
@ -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} ! \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user