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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn source_profile_stays_pass_through_without_explicit_reencode_request() {
|
fn source_profile_stays_pass_through_without_explicit_reencode_request() {
|
||||||
@ -743,4 +744,153 @@ mod tests {
|
|||||||
assert!(bitrate_request.reencode);
|
assert!(bitrate_request.reencode);
|
||||||
assert!(fps_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