lesavka: add eye decoder comparison probe
This commit is contained in:
parent
e3cb555c90
commit
377cda1309
38
scripts/manual/compare-eye-decoders.sh
Executable file
38
scripts/manual/compare-eye-decoders.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DEVICE="${1:-/dev/lesavka_r_eye}"
|
||||
OUTDIR="${2:-/tmp/lesavka-decoder-probe}"
|
||||
WAIT_SECONDS="${LESAVKA_DECODER_PROBE_WAIT_SECONDS:-3600}"
|
||||
BUFFERS="${LESAVKA_DECODER_PROBE_BUFFERS:-120}"
|
||||
POLL_SECONDS="${LESAVKA_DECODER_PROBE_POLL_SECONDS:-1}"
|
||||
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
deadline=$((SECONDS + WAIT_SECONDS))
|
||||
while [[ ! -e "$DEVICE" ]]; do
|
||||
if (( SECONDS >= deadline )); then
|
||||
echo "decoder probe timed out waiting for $DEVICE" >&2
|
||||
exit 124
|
||||
fi
|
||||
sleep "$POLL_SECONDS"
|
||||
done
|
||||
|
||||
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
prefix="$OUTDIR/${timestamp}-$(basename "$DEVICE")"
|
||||
|
||||
echo "capturing decoder comparison for $DEVICE into $prefix.*"
|
||||
|
||||
gst-launch-1.0 -e \
|
||||
v4l2src device="$DEVICE" io-mode=mmap do-timestamp=true num-buffers="$BUFFERS" ! \
|
||||
queue max-size-buffers=8 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||
h264parse disable-passthrough=true config-interval=-1 ! \
|
||||
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||
tee name=t \
|
||||
t. ! queue ! filesink location="${prefix}-source.h264" \
|
||||
t. ! queue ! avdec_h264 ! videoconvert ! video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
|
||||
pngenc snapshot=true ! filesink location="${prefix}-avdec.png" \
|
||||
t. ! queue ! openh264dec ! videoconvert ! video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
|
||||
pngenc snapshot=true ! filesink location="${prefix}-openh264.png"
|
||||
|
||||
echo "decoder comparison artifacts written under $OUTDIR"
|
||||
@ -714,6 +714,7 @@ pub async fn eye_ball_with_request(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
fn source_profile_stays_pass_through_without_explicit_reencode_request() {
|
||||
@ -743,4 +744,153 @@ mod tests {
|
||||
assert!(bitrate_request.reencode);
|
||||
assert!(fps_request.reencode);
|
||||
}
|
||||
|
||||
fn marker_frame(width: i32, height: i32) -> Vec<u8> {
|
||||
let mut rgba = vec![0_u8; (width * height * 4) as usize];
|
||||
let marker = 96;
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let idx = ((y * width + x) * 4) as usize;
|
||||
let (r, g, b) = if x < marker && y < marker {
|
||||
(255, 0, 0)
|
||||
} else if x >= width - marker && y < marker {
|
||||
(0, 255, 0)
|
||||
} else if x < marker && y >= height - marker {
|
||||
(0, 0, 255)
|
||||
} else if x >= width - marker && y >= height - marker {
|
||||
(255, 255, 0)
|
||||
} else {
|
||||
(24, 24, 24)
|
||||
};
|
||||
rgba[idx..idx + 4].copy_from_slice(&[r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
fn pull_reencoded_frame_rgba(
|
||||
width: i32,
|
||||
height: i32,
|
||||
input_fps: u32,
|
||||
output_fps: u32,
|
||||
) -> anyhow::Result<(i32, i32, Vec<u8>)> {
|
||||
gst::init().context("gst init")?;
|
||||
let desc = format!(
|
||||
"appsrc name=src is-live=false format=time block=true ! \
|
||||
videoconvert ! video/x-raw,format=I420,width={width},height={height},framerate={input_fps}/1,pixel-aspect-ratio=1/1 ! \
|
||||
x264enc tune=zerolatency speed-preset=veryfast bitrate=12000 key-int-max={input_fps} option-string=sar=1/1 ! \
|
||||
h264parse disable-passthrough=true config-interval=-1 ! \
|
||||
avdec_h264 ! videoconvert ! videoscale add-borders=false ! videorate ! \
|
||||
video/x-raw,format=I420,width={width},height={height},framerate={output_fps}/1,pixel-aspect-ratio=1/1 ! \
|
||||
x264enc tune=zerolatency speed-preset=faster bitrate=12000 key-int-max=5 option-string=sar=1/1 ! \
|
||||
h264parse disable-passthrough=true config-interval=-1 ! \
|
||||
avdec_h264 ! videoconvert ! video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
|
||||
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
|
||||
);
|
||||
let pipeline = gst::parse::launch(&desc)?
|
||||
.downcast::<gst::Pipeline>()
|
||||
.expect("pipeline");
|
||||
let appsrc = pipeline
|
||||
.by_name("src")
|
||||
.expect("appsrc")
|
||||
.downcast::<gst_app::AppSrc>()
|
||||
.expect("appsrc cast");
|
||||
appsrc.set_caps(Some(
|
||||
&gst::Caps::builder("video/x-raw")
|
||||
.field("format", &"RGBA")
|
||||
.field("width", &width)
|
||||
.field("height", &height)
|
||||
.field("framerate", &gst::Fraction::new(input_fps as i32, 1))
|
||||
.field("pixel-aspect-ratio", &gst::Fraction::new(1, 1))
|
||||
.build(),
|
||||
));
|
||||
appsrc.set_format(gst::Format::Time);
|
||||
let appsink = pipeline
|
||||
.by_name("sink")
|
||||
.expect("appsink")
|
||||
.downcast::<gst_app::AppSink>()
|
||||
.expect("appsink cast");
|
||||
appsink.set_caps(Some(
|
||||
&gst::Caps::builder("video/x-raw")
|
||||
.field("format", &"RGBA")
|
||||
.field("pixel-aspect-ratio", &gst::Fraction::new(1, 1))
|
||||
.build(),
|
||||
));
|
||||
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.context("starting reencode probe pipeline")?;
|
||||
|
||||
let mut buffer = gst::Buffer::from_mut_slice(marker_frame(width, height));
|
||||
if let Some(buf) = buffer.get_mut() {
|
||||
buf.set_pts(Some(gst::ClockTime::ZERO));
|
||||
buf.set_duration(Some(
|
||||
gst::ClockTime::from_nseconds(1_000_000_000_u64 / input_fps.max(1) as u64),
|
||||
));
|
||||
}
|
||||
appsrc
|
||||
.push_buffer(buffer)
|
||||
.map_err(|err| anyhow::anyhow!("push buffer failed: {err:?}"))?;
|
||||
appsrc
|
||||
.end_of_stream()
|
||||
.map_err(|err| anyhow::anyhow!("eos failed: {err:?}"))?;
|
||||
|
||||
let sample = appsink
|
||||
.try_pull_sample(gst::ClockTime::from_seconds(5))
|
||||
.ok_or_else(|| anyhow::anyhow!("timed out pulling reencoded frame"))?;
|
||||
let caps = sample.caps().ok_or_else(|| anyhow::anyhow!("missing sample caps"))?;
|
||||
let structure = caps
|
||||
.structure(0)
|
||||
.ok_or_else(|| anyhow::anyhow!("missing caps structure"))?;
|
||||
let out_width = structure
|
||||
.get::<i32>("width")
|
||||
.map_err(|err| anyhow::anyhow!("missing output width: {err}"))?;
|
||||
let out_height = structure
|
||||
.get::<i32>("height")
|
||||
.map_err(|err| anyhow::anyhow!("missing output height: {err}"))?;
|
||||
let buffer = sample
|
||||
.buffer()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing sample buffer"))?;
|
||||
let map = buffer
|
||||
.map_readable()
|
||||
.map_err(|_| anyhow::anyhow!("sample map failed"))?;
|
||||
let rgba = map.as_slice().to_vec();
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
Ok((out_width, out_height, rgba))
|
||||
}
|
||||
|
||||
fn rgba_pixel(rgba: &[u8], width: i32, x: i32, y: i32) -> (u8, u8, u8) {
|
||||
let idx = ((y * width + x) * 4) as usize;
|
||||
(rgba[idx], rgba[idx + 1], rgba[idx + 2])
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn reencode_probe_preserves_corner_markers_on_full_frame_content() {
|
||||
let (width, height, rgba) =
|
||||
pull_reencoded_frame_rgba(1920, 1080, 60, 30).expect("probe frame");
|
||||
assert_eq!((width, height), (1920, 1080));
|
||||
|
||||
let top_left = rgba_pixel(&rgba, width, 24, 24);
|
||||
let top_right = rgba_pixel(&rgba, width, width - 25, 24);
|
||||
let bottom_left = rgba_pixel(&rgba, width, 24, height - 25);
|
||||
let bottom_right = rgba_pixel(&rgba, width, width - 25, height - 25);
|
||||
|
||||
assert!(
|
||||
top_left.0 > 180 && top_left.1 < 80 && top_left.2 < 80,
|
||||
"top-left marker drifted: {top_left:?}"
|
||||
);
|
||||
assert!(
|
||||
top_right.1 > 180 && top_right.0 < 80 && top_right.2 < 80,
|
||||
"top-right marker drifted: {top_right:?}"
|
||||
);
|
||||
assert!(
|
||||
bottom_left.2 > 180 && bottom_left.0 < 80 && bottom_left.1 < 80,
|
||||
"bottom-left marker drifted: {bottom_left:?}"
|
||||
);
|
||||
assert!(
|
||||
bottom_right.0 > 180 && bottom_right.1 > 180 && bottom_right.2 < 120,
|
||||
"bottom-right marker drifted: {bottom_right:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user