diff --git a/.ci/test.sh b/.ci/test.sh index 95f834b..6eb99d4 100755 --- a/.ci/test.sh +++ b/.ci/test.sh @@ -48,6 +48,8 @@ VERBOSE=false HARDWARE_AVAILABLE=false HARDWARE_CHECK_DONE=false GMSL_CAMERA=false +GMSL_CAMERA_SN="" # Serial number of the first available GMSL stereo camera +XONE_CAMERA_SN="" # Serial number of the first available ZED X One (mono) camera ZERO_COPY_AVAILABLE=false # Counters @@ -130,12 +132,19 @@ test_pass() { test_fail() { local test_name="$1" - local details="$2" + local output="$2" TESTS_RUN=$((TESTS_RUN + 1)) TESTS_FAILED=$((TESTS_FAILED + 1)) log_error "$test_name" - if [ "$VERBOSE" = true ] && [ -n "$details" ]; then - echo " Details: $details" + # Always show failure context so failures are actionable without -v + if [ -n "$output" ]; then + echo " ---- failure details ----" + echo "$output" | grep -i "error\|fail\|critical\|abort\|timeout\|refused" | head -8 | sed 's/^/ /' + # If no error lines matched, show last 4 lines for context + if ! echo "$output" | grep -qi "error\|fail\|critical\|abort"; then + echo "$output" | tail -4 | sed 's/^/ /' + fi + echo " -------------------------" fi } @@ -225,6 +234,39 @@ check_hardware() { if echo "$zed_output" | grep -qi "ZED X\|GMSL"; then GMSL_CAMERA=true log_info "GMSL camera detected (ZED X / ZED X Mini)" + + # Extract the serial number of the first available GMSL *stereo* + # camera (ZED X or ZED X Mini — NOT ZED XOne which is mono). + # ZED_Explorer output groups cameras in "## Cam N ##" blocks. + # We look for a block containing a stereo GMSL model AND "AVAILABLE". + GMSL_CAMERA_SN=$(echo "$zed_output" | awk ' + /^## Cam/ { block=""; sn=""; is_gmsl=0; is_stereo=0; is_avail=0 } + /Model.*"ZED X"$/ || /Model.*"ZED X Mini"/ { is_stereo=1 } + /Type.*"GMSL"/ { is_gmsl=1 } + /State.*"AVAILABLE"/ { is_avail=1 } + /S\/N/ { sn=$NF } + /^\*\*\*\*\*/ { + if (is_gmsl && is_stereo && is_avail && sn != "") { print sn; exit } + } + ') + if [ -n "$GMSL_CAMERA_SN" ]; then + log_info "First available GMSL stereo camera S/N: $GMSL_CAMERA_SN" + fi + + # Also extract the S/N of the first available ZED X One (mono) camera. + XONE_CAMERA_SN=$(echo "$zed_output" | awk ' + /^## Cam/ { sn=""; is_gmsl=0; is_xone=0; is_avail=0 } + /Model.*"ZED XOne/ { is_xone=1 } + /Type.*"GMSL"/ { is_gmsl=1 } + /State.*"AVAILABLE"/ { is_avail=1 } + /S\/N/ { sn=$NF } + /^\*\*\*\*\*/ { + if (is_gmsl && is_xone && is_avail && sn != "") { print sn; exit } + } + ') + if [ -n "$XONE_CAMERA_SN" ]; then + log_info "First available ZED X One camera S/N: $XONE_CAMERA_SN" + fi fi fi fi @@ -267,6 +309,73 @@ check_hardware() { fi } +# Build the best available H.264 encode pipeline for the current platform. +# Priority order: +# 1. NV12 zero-copy → nvv4l2h264enc (Jetson + GMSL, no CPU conversion) +# 2. BGRA → nvvidconv → nvv4l2h264enc (Jetson without zero-copy) +# 3. BGRA → videoconvert → x264enc (x86 / USB fallback) +# 4. BGRA → videoconvert → openh264enc (last resort) +# +# Sets the following globals: +# _ENCODE_STREAM_TYPE - zedsrc stream-type value (0 or 6) +# _ENCODE_PIPELINE - encoder chain (everything after zedsrc ! queue) +# _ENCODER_NAME - human-readable encoder name for test output +# _ENCODE_AVAILABLE - "true" if an encoder was found +# _ENCODE_CAMERA_PROP - extra zedsrc prop to target a GMSL camera (may be empty) +# +# For RTSP pipelines (pass "rtsp" as $1), appends h264parse + rtph264pay. +build_h264_encode_pipeline() { + local mode="${1:-encode}" # "encode" or "rtsp" + + _ENCODE_STREAM_TYPE=0 + _ENCODE_PIPELINE="" + _ENCODER_NAME="" + _ENCODE_AVAILABLE=false + _ENCODE_CAMERA_PROP="" + + if [ -f /etc/nv_tegra_release ] && gst-inspect-1.0 nvv4l2h264enc &>/dev/null; then + if [ "$ZERO_COPY_AVAILABLE" = true ]; then + # Best path: NV12 NVMM straight from the camera into HW encoder. + # No queue, no conversion — zedsrc already outputs NVMM NV12. + _ENCODE_STREAM_TYPE=6 + _ENCODE_PIPELINE="video/x-raw(memory:NVMM),format=NV12 ! nvv4l2h264enc bitrate=4000000" + _ENCODER_NAME="nvv4l2h264enc (NV12 ZC)" + # NV12 zero-copy requires a GMSL camera. When both USB and GMSL + # cameras are present, zedsrc without an explicit camera-sn would + # pick the first available camera (which may be USB). Pin to a + # GMSL stereo camera so the pipeline actually gets NVMM buffers. + if [ -n "$GMSL_CAMERA_SN" ]; then + _ENCODE_CAMERA_PROP="camera-sn=$GMSL_CAMERA_SN" + fi + elif gst-inspect-1.0 nvvidconv &>/dev/null; then + # Fallback on Jetson: upload BGRA to NVMM and convert + _ENCODE_STREAM_TYPE=0 + _ENCODE_PIPELINE="nvvidconv ! video/x-raw(memory:NVMM),format=I420 ! nvv4l2h264enc bitrate=4000000" + _ENCODER_NAME="nvv4l2h264enc" + fi + fi + + # CPU-based encoders for x86 / USB cameras / missing HW encoder + if [ -z "$_ENCODE_PIPELINE" ]; then + if gst-inspect-1.0 x264enc &>/dev/null; then + _ENCODE_STREAM_TYPE=0 + _ENCODE_PIPELINE="videoconvert ! video/x-raw,format=I420 ! x264enc tune=zerolatency speed-preset=ultrafast bitrate=2000" + _ENCODER_NAME="x264enc" + elif gst-inspect-1.0 openh264enc &>/dev/null; then + _ENCODE_STREAM_TYPE=0 + _ENCODE_PIPELINE="videoconvert ! video/x-raw,format=I420 ! openh264enc" + _ENCODER_NAME="openh264enc" + fi + fi + + if [ -n "$_ENCODE_PIPELINE" ]; then + _ENCODE_AVAILABLE=true + if [ "$mode" = "rtsp" ]; then + _ENCODE_PIPELINE="$_ENCODE_PIPELINE ! h264parse ! rtph264pay name=pay0 pt=96" + fi + fi +} + # ============================================================================= # Test Functions # ============================================================================= @@ -304,9 +413,9 @@ test_plugin_registration() { for plugin in "${PLUGINS[@]}"; do if gst-inspect-1.0 "$plugin" > /dev/null 2>&1; then - test_pass "Plugin '$plugin' is registered" + test_pass "[PR01] Plugin '$plugin' is registered" else - test_fail "Plugin '$plugin' is registered" + test_fail "[PR02] Plugin '$plugin' is registered" fi done } @@ -318,9 +427,9 @@ test_plugin_properties() { local zedsrc_props=("camera-resolution" "camera-fps" "stream-type" "depth-mode" "od-enabled" "bt-enabled") for prop in "${zedsrc_props[@]}"; do if gst-inspect-1.0 zedsrc 2>&1 | grep -q "$prop"; then - test_pass "zedsrc has property '$prop'" + test_pass "[PP01] zedsrc has property '$prop'" else - test_fail "zedsrc has property '$prop'" + test_fail "[PP02] zedsrc has property '$prop'" fi done @@ -328,9 +437,9 @@ test_plugin_properties() { local zedxonesrc_props=("camera-resolution" "camera-fps" "camera-id") for prop in "${zedxonesrc_props[@]}"; do if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "$prop"; then - test_pass "zedxonesrc has property '$prop'" + test_pass "[PP03] zedxonesrc has property '$prop'" else - test_fail "zedxonesrc has property '$prop'" + test_fail "[PP04] zedxonesrc has property '$prop'" fi done @@ -338,9 +447,9 @@ test_plugin_properties() { local zeddemux_props=("is-depth" "stream-data" "is-mono") for prop in "${zeddemux_props[@]}"; do if gst-inspect-1.0 zeddemux 2>&1 | grep -q "$prop"; then - test_pass "zeddemux has property '$prop'" + test_pass "[PP05] zeddemux has property '$prop'" else - test_fail "zeddemux has property '$prop'" + test_fail "[PP06] zeddemux has property '$prop'" fi done @@ -348,9 +457,9 @@ test_plugin_properties() { local csvsink_props=("location" "append") for prop in "${csvsink_props[@]}"; do if gst-inspect-1.0 zeddatacsvsink 2>&1 | grep -q "$prop"; then - test_pass "zeddatacsvsink has property '$prop'" + test_pass "[PP07] zeddatacsvsink has property '$prop'" else - test_fail "zeddatacsvsink has property '$prop'" + test_fail "[PP08] zeddatacsvsink has property '$prop'" fi done } @@ -361,9 +470,9 @@ test_plugin_factory() { # Check that each plugin has valid factory details for plugin in "${PLUGINS[@]}"; do if gst-inspect-1.0 "$plugin" 2>&1 | grep -q "Factory Details"; then - test_pass "Plugin '$plugin' has valid factory" + test_pass "[PF01] Plugin '$plugin' has valid factory" else - test_fail "Plugin '$plugin' has valid factory" + test_fail "[PF02] Plugin '$plugin' has valid factory" fi done } @@ -375,9 +484,9 @@ test_zedsrc_enums() { local resolutions=("HD2K" "HD1080" "HD1200" "HD720" "SVGA" "VGA") for res in "${resolutions[@]}"; do if gst-inspect-1.0 zedsrc 2>&1 | grep -q "$res"; then - test_pass "zedsrc has resolution '$res'" + test_pass "[PE01] zedsrc has resolution '$res'" else - test_fail "zedsrc has resolution '$res'" + test_fail "[PE02] zedsrc has resolution '$res'" fi done @@ -385,9 +494,9 @@ test_zedsrc_enums() { local depth_modes=("NONE" "PERFORMANCE" "QUALITY" "ULTRA" "NEURAL") for mode in "${depth_modes[@]}"; do if gst-inspect-1.0 zedsrc 2>&1 | grep -q "$mode"; then - test_pass "zedsrc has depth mode '$mode'" + test_pass "[PE03] zedsrc has depth mode '$mode'" else - test_fail "zedsrc has depth mode '$mode'" + test_fail "[PE04] zedsrc has depth mode '$mode'" fi done @@ -395,19 +504,159 @@ test_zedsrc_enums() { local stream_types=("Left image" "Right image" "Stereo couple" "Depth image") for stype in "${stream_types[@]}"; do if gst-inspect-1.0 zedsrc 2>&1 | grep -q "$stype"; then - test_pass "zedsrc has stream-type '$stype'" + test_pass "[PE05] zedsrc has stream-type '$stype'" else - test_fail "zedsrc has stream-type '$stype'" + test_fail "[PE06] zedsrc has stream-type '$stype'" fi done } +test_zedsrc_auto_resolution() { + print_subheader "ZedSrc Auto-Resolution Negotiation Tests" + + # Check if zedsrc is available + if ! gst-inspect-1.0 zedsrc > /dev/null 2>&1; then + skip_test "[AR01] ZedSrc auto-resolution tests" "zedsrc not available" + return 0 + fi + + # --- Enum introspection --- + # AUTO resolution enum must be advertised + if gst-inspect-1.0 zedsrc 2>&1 | grep -qi "Automatic.*Default value\|AUTO_RES\|auto_res"; then + test_pass "[AR02] zedsrc has AUTO resolution enum" + else + # Accept either "Automatic" or "AUTO" keyword + if gst-inspect-1.0 zedsrc 2>&1 | grep -qi "automatic"; then + test_pass "[AR03] zedsrc has AUTO resolution enum" + else + test_fail "[AR04] zedsrc has AUTO resolution enum" + fi + fi + + # Default camera-resolution must be AUTO (enum value 6) + local default_res + default_res=$(gst-inspect-1.0 zedsrc 2>&1 | grep -A2 "camera-resolution" | grep -oi "Default.*" | head -1) + if echo "$default_res" | grep -qi "6\|auto\|automatic"; then + test_pass "[AR05] zedsrc default camera-resolution is AUTO" + else + test_fail "[AR06] zedsrc default camera-resolution is AUTO (got: $default_res)" + fi + + # Default camera-fps must be 30 + local default_fps + default_fps=$(gst-inspect-1.0 zedsrc 2>&1 | grep -A2 "camera-fps" | grep -oi "Default.*" | head -1) + if echo "$default_fps" | grep -q "30"; then + test_pass "[AR07] zedsrc default camera-fps is 30" + else + test_fail "[AR08] zedsrc default camera-fps is 30 (got: $default_fps)" + fi + + # --- FPS discrete list in caps (requires camera) --- + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[AR09] zedsrc AUTO caps inspection" "No camera detected" + return 0 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[AR10] zedsrc AUTO caps negotiation" "Extensive mode required" + return 0 + fi + + local timeout_val=90 + local num_buffers=5 + local output + + # Test 1: Default (AUTO + 30fps) should produce a valid pipeline + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[AR11] zedsrc AUTO resolution default pipeline" + else + test_fail "[AR12] zedsrc AUTO resolution default pipeline" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # Test 2: AUTO + 15fps → HD2K on USB, HD1200 on GMSL + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=6 camera-fps=15 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + if [ "$GMSL_CAMERA" = true ]; then + test_pass "[AR13] zedsrc AUTO@15fps pipeline works (GMSL → HD1200)" + else + test_pass "[AR14] zedsrc AUTO@15fps pipeline works (USB → HD2K)" + fi + else + test_fail "[AR15] zedsrc AUTO@15fps pipeline" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # Test 3: AUTO + 60fps → should select HD1200 (highest supporting 60) + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=6 camera-fps=60 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[AR16] zedsrc AUTO@60fps pipeline works" + else + test_fail "[AR17] zedsrc AUTO@60fps pipeline" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # Test 4: AUTO + 120fps → should select SVGA (only mode supporting 120) + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=6 camera-fps=120 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[AR18] zedsrc AUTO@120fps pipeline works" + else + # 120fps is GMSL-only, USB cameras will clamp to closest supported FPS + if echo "$output" | grep -qi "no camera\|not found\|failed to open\|not supported"; then + skip_test "[AR19] zedsrc AUTO@120fps pipeline" "Not supported on this camera" + else + test_fail "[AR20] zedsrc AUTO@120fps pipeline" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 5: Explicit resolution + FPS clamping + if [ "$GMSL_CAMERA" = true ]; then + # GMSL: test HD1200 → supports 15/30/60, so 120 clamps to 60 + # Pin to the GMSL camera — HD1200 is ZED X-only, not available on USB cameras + local gmsl_prop="" + [ -n "$GMSL_CAMERA_SN" ] && gmsl_prop="camera-sn=$GMSL_CAMERA_SN" + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=2 camera-fps=120 $gmsl_prop num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[AR21] zedsrc HD1200@120fps clamped to 60fps [GMSL]" + else + test_fail "[AR22] zedsrc HD1200@120fps FPS clamping [GMSL]" "$output" + fi + else + # USB: test HD2K → only supports 15, so 60 clamps to 15 + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=0 camera-fps=60 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[AR23] zedsrc HD2K@60fps clamped to 15fps [USB]" + else + test_fail "[AR24] zedsrc HD2K@60fps FPS clamping [USB]" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 6: Downstream size negotiation — request smaller output than camera native + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=6 num-buffers=$num_buffers ! \ + "video/x-raw,width=640,height=480" ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[AR25] zedsrc AUTO with downstream resize (640x480)" + else + test_fail "[AR26] zedsrc AUTO with downstream resize (640x480)" "$output" + fi + + sleep $CAMERA_RESET_DELAY +} + test_zedsrc_nv12() { print_subheader "ZedSrc NV12 Zero-Copy Tests" # Check if zedsrc is available if ! gst-inspect-1.0 zedsrc > /dev/null 2>&1; then - skip_test "ZedSrc NV12 tests" "zedsrc not available" + skip_test "[NV01] ZedSrc NV12 tests" "zedsrc not available" return 0 fi @@ -415,37 +664,37 @@ test_zedsrc_nv12() { local zedsrc_zerocopy_available=false if gst-inspect-1.0 zedsrc 2>&1 | grep -q "Raw NV12 zero-copy"; then zedsrc_zerocopy_available=true - test_pass "zedsrc supports NV12 zero-copy (stream-type=6/7)" + test_pass "[NV02] zedsrc supports NV12 zero-copy (stream-type=6/7)" else - skip_test "zedsrc NV12 zero-copy" "SDK < 5.2 or not built with Advanced Capture API" + skip_test "[NV03] zedsrc NV12 zero-copy" "SDK < 5.2 or not built with Advanced Capture API" fi # Check for AUTO stream type with NV12 preference if gst-inspect-1.0 zedsrc 2>&1 | grep -q "Auto.*NV12 zero-copy\|prefer NV12 zero-copy"; then - test_pass "zedsrc has STREAM_AUTO with NV12 preference" + test_pass "[NV04] zedsrc has STREAM_AUTO with NV12 preference" else # May not have zero-copy but should still have AUTO if gst-inspect-1.0 zedsrc 2>&1 | grep -q "Auto-negotiate"; then - test_pass "zedsrc has STREAM_AUTO" + test_pass "[NV05] zedsrc has STREAM_AUTO" else - test_fail "zedsrc has STREAM_AUTO" + test_fail "[NV06] zedsrc has STREAM_AUTO" fi fi # Hardware test for NV12 zero-copy if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "ZedSrc NV12 hardware test" "No camera detected" + skip_test "[NV07] ZedSrc NV12 hardware test" "No camera detected" return 2 fi # Only test on Jetson with zero-copy available if [ ! -f /etc/nv_tegra_release ]; then - skip_test "ZedSrc NV12 hardware test" "Not a Jetson platform" + skip_test "[NV08] ZedSrc NV12 hardware test" "Not a Jetson platform" return 0 fi if [ "$zedsrc_zerocopy_available" = false ]; then - skip_test "ZedSrc NV12 hardware test" "Zero-copy not available" + skip_test "[NV09] ZedSrc NV12 hardware test" "Zero-copy not available" return 0 fi @@ -453,40 +702,41 @@ test_zedsrc_nv12() { local timeout_val=30 local num_buffers=10 local output + # Pin to a GMSL camera when USB cameras are also present + local gmsl_prop="" + [ -n "$GMSL_CAMERA_SN" ] && gmsl_prop="camera-sn=$GMSL_CAMERA_SN" # Test stream-type=6 (single NV12 zero-copy) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=6 num-buffers=$num_buffers ! \ + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=6 $gmsl_prop num-buffers=$num_buffers ! \ 'video/x-raw(memory:NVMM),format=NV12' ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "ZedSrc NV12 zero-copy pipeline (stream-type=6)" + test_pass "[NV10] ZedSrc NV12 zero-copy pipeline (stream-type=6)" else if echo "$output" | grep -qi "no camera\|not found\|failed to open\|not supported"; then - skip_test "ZedSrc NV12 zero-copy pipeline" "Not supported on this camera model" + skip_test "[NV11] ZedSrc NV12 zero-copy pipeline" "Not supported on this camera model" else - test_fail "ZedSrc NV12 zero-copy pipeline (stream-type=6)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[NV12] ZedSrc NV12 zero-copy pipeline (stream-type=6)" "$output" fi fi sleep $CAMERA_RESET_DELAY # Test stream-type=7 (stereo NV12 zero-copy) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=7 num-buffers=$num_buffers ! \ + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=7 $gmsl_prop num-buffers=$num_buffers ! \ 'video/x-raw(memory:NVMM),format=NV12' ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "ZedSrc NV12 stereo zero-copy pipeline (stream-type=7)" + test_pass "[NV13] ZedSrc NV12 stereo zero-copy pipeline (stream-type=7)" else if echo "$output" | grep -qi "no camera\|not found\|failed to open\|not supported"; then - skip_test "ZedSrc NV12 stereo zero-copy pipeline" "Not supported on this camera model" + skip_test "[NV14] ZedSrc NV12 stereo zero-copy pipeline" "Not supported on this camera model" else - test_fail "ZedSrc NV12 stereo zero-copy pipeline (stream-type=7)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[NV15] ZedSrc NV12 stereo zero-copy pipeline (stream-type=7)" "$output" fi fi sleep $CAMERA_RESET_DELAY else - skip_test "ZedSrc NV12 hardware test" "Extensive mode required" + skip_test "[NV16] ZedSrc NV12 hardware test" "Extensive mode required" fi } @@ -495,29 +745,29 @@ test_element_pads() { # Check zedsrc has src pad if gst-inspect-1.0 zedsrc 2>&1 | grep -q "SRC template"; then - test_pass "zedsrc has SRC pad template" + test_pass "[PT01] zedsrc has SRC pad template" else - test_fail "zedsrc has SRC pad template" + test_fail "[PT02] zedsrc has SRC pad template" fi # Check zeddemux has sink and src pads if gst-inspect-1.0 zeddemux 2>&1 | grep -q "SINK template"; then - test_pass "zeddemux has SINK pad template" + test_pass "[PT03] zeddemux has SINK pad template" else - test_fail "zeddemux has SINK pad template" + test_fail "[PT04] zeddemux has SINK pad template" fi if gst-inspect-1.0 zeddemux 2>&1 | grep -q "SRC template"; then - test_pass "zeddemux has SRC pad template" + test_pass "[PT05] zeddemux has SRC pad template" else - test_fail "zeddemux has SRC pad template" + test_fail "[PT06] zeddemux has SRC pad template" fi # Check zedodoverlay is a transform element if gst-inspect-1.0 zedodoverlay 2>&1 | grep -q "SINK template"; then - test_pass "zedodoverlay has SINK pad template" + test_pass "[PT07] zedodoverlay has SINK pad template" else - test_fail "zedodoverlay has SINK pad template" + test_fail "[PT08] zedodoverlay has SINK pad template" fi } @@ -525,7 +775,7 @@ test_hardware_basic() { print_subheader "Hardware Basic Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Hardware grab test" "No camera detected" + skip_test "[HB01] Hardware grab test" "No camera detected" return 2 fi @@ -540,12 +790,9 @@ test_hardware_basic() { output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc num-buffers=5 ! fakesink 2>&1) local exit_code=$? if [ $exit_code -eq 0 ]; then - test_pass "ZED camera basic grab" + test_pass "[HB02] ZED camera basic grab" else - test_fail "ZED camera basic grab" - if [ "$VERBOSE" = true ]; then - echo "$output" | grep -i "error\|fail" | head -5 - fi + test_fail "[HB03] ZED camera basic grab" "$output" fi # Allow camera to reset before next test @@ -556,8 +803,8 @@ test_hardware_streaming() { print_subheader "Hardware Streaming Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Stream type 0 (left only)" "No camera detected" - skip_test "Stream type 2 (left+right)" "No camera detected" + skip_test "[HS01] Stream type 0 (left only)" "No camera detected" + skip_test "[HS02] Stream type 2 (left+right)" "No camera detected" return 2 fi @@ -573,20 +820,18 @@ test_hardware_streaming() { local output output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Stream type 0 (left only)" + test_pass "[HS03] Stream type 0 (left only)" else - test_fail "Stream type 0 (left only)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[HS04] Stream type 0 (left only)" "$output" fi sleep $CAMERA_RESET_DELAY output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=2 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Stream type 2 (left+right)" + test_pass "[HS05] Stream type 2 (left+right)" else - test_fail "Stream type 2 (left+right)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[HS06] Stream type 2 (left+right)" "$output" fi if [ "$EXTENSIVE_MODE" = true ]; then @@ -594,20 +839,18 @@ test_hardware_streaming() { output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=3 depth-mode=1 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Stream type 3 (depth 16-bit)" + test_pass "[HS07] Stream type 3 (depth 16-bit)" else - test_fail "Stream type 3 (depth 16-bit)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[HS08] Stream type 3 (depth 16-bit)" "$output" fi sleep $CAMERA_RESET_DELAY output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=4 depth-mode=1 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Stream type 4 (left+depth)" + test_pass "[HS09] Stream type 4 (left+depth)" else - test_fail "Stream type 4 (left+depth)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[HS10] Stream type 4 (left+depth)" "$output" fi fi @@ -617,40 +860,42 @@ test_hardware_streaming() { if [ "$ZERO_COPY_AVAILABLE" = true ]; then sleep $CAMERA_RESET_DELAY + # Pin to a GMSL camera when USB cameras are also present + local gmsl_prop="" + [ -n "$GMSL_CAMERA_SN" ] && gmsl_prop="camera-sn=$GMSL_CAMERA_SN" + # Test stream-type=6 (single NV12 zero-copy) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=6 num-buffers=$num_buffers ! \ + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=6 $gmsl_prop num-buffers=$num_buffers ! \ 'video/x-raw(memory:NVMM),format=NV12' ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Stream type 6 (RAW_NV12 zero-copy)" + test_pass "[HS11] Stream type 6 (RAW_NV12 zero-copy)" else - test_fail "Stream type 6 (RAW_NV12 zero-copy)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[HS12] Stream type 6 (RAW_NV12 zero-copy)" "$output" fi if [ "$EXTENSIVE_MODE" = true ]; then sleep $CAMERA_RESET_DELAY # Test stream-type=7 (stereo NV12 zero-copy) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=7 num-buffers=$num_buffers ! \ + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=7 $gmsl_prop num-buffers=$num_buffers ! \ 'video/x-raw(memory:NVMM),format=NV12' ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Stream type 7 (RAW_NV12 stereo zero-copy)" + test_pass "[HS13] Stream type 7 (RAW_NV12 stereo zero-copy)" else - test_fail "Stream type 7 (RAW_NV12 stereo zero-copy)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[HS14] Stream type 7 (RAW_NV12 stereo zero-copy)" "$output" fi fi elif [ "$GMSL_CAMERA" = true ]; then # GMSL camera present but zero-copy not available (older SDK) - skip_test "Stream type 6 (RAW_NV12)" "Requires ZED SDK 5.2+ with Advanced Capture API" - skip_test "Stream type 7 (RAW_NV12 stereo)" "Requires ZED SDK 5.2+ with Advanced Capture API" + skip_test "[HS15] Stream type 6 (RAW_NV12)" "Requires ZED SDK 5.2+ with Advanced Capture API" + skip_test "[HS16] Stream type 7 (RAW_NV12 stereo)" "Requires ZED SDK 5.2+ with Advanced Capture API" elif [ "$HARDWARE_AVAILABLE" = true ]; then # USB camera - zero-copy not supported - skip_test "Stream type 6 (RAW_NV12)" "USB camera - GMSL camera required" - skip_test "Stream type 7 (RAW_NV12 stereo)" "USB camera - GMSL camera required" + skip_test "[HS17] Stream type 6 (RAW_NV12)" "USB camera - GMSL camera required" + skip_test "[HS18] Stream type 7 (RAW_NV12 stereo)" "USB camera - GMSL camera required" else - skip_test "Stream type 6 (RAW_NV12)" "No camera detected" - skip_test "Stream type 7 (RAW_NV12 stereo)" "No camera detected" + skip_test "[HS19] Stream type 6 (RAW_NV12)" "No camera detected" + skip_test "[HS20] Stream type 7 (RAW_NV12 stereo)" "No camera detected" fi fi @@ -661,15 +906,15 @@ test_hardware_demux() { print_subheader "Hardware Demux Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Demux left/right" "No camera detected" - skip_test "Demux left/depth" "No camera detected" + skip_test "[DM01] Demux left/right" "No camera detected" + skip_test "[DM02] Demux left/depth" "No camera detected" return 2 fi # Demux tests can hang with limited buffers due to EOS propagation issues # Only run in extensive mode where we have longer timeouts if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Demux left/right stream" "Extensive mode required (demux needs longer runtime)" + skip_test "[DM03] Demux left/right stream" "Extensive mode required (demux needs longer runtime)" return 0 fi @@ -681,10 +926,9 @@ test_hardware_demux() { output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=2 num-buffers=$num_buffers ! \ zeddemux is-depth=false name=demux demux.src_left ! queue ! fakesink demux.src_aux ! queue ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Demux left/right stream" + test_pass "[DM04] Demux left/right stream" else - test_fail "Demux left/right stream" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[DM05] Demux left/right stream" "$output" fi sleep $CAMERA_RESET_DELAY @@ -692,10 +936,9 @@ test_hardware_demux() { output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=4 depth-mode=1 num-buffers=$num_buffers ! \ zeddemux is-depth=true name=demux demux.src_left ! queue ! fakesink demux.src_aux ! queue ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Demux left/depth stream" + test_pass "[DM06] Demux left/depth stream" else - test_fail "Demux left/depth stream" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[DM07] Demux left/depth stream" "$output" fi sleep $CAMERA_RESET_DELAY @@ -705,12 +948,12 @@ test_hardware_od() { print_subheader "Hardware Object Detection Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Object Detection pipeline" "No camera detected" + skip_test "[OD01] Object Detection pipeline" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Object Detection pipeline" "Extensive mode required" + skip_test "[OD02] Object Detection pipeline" "Extensive mode required" return 0 fi @@ -719,9 +962,9 @@ test_hardware_od() { if timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 od-enabled=true od-detection-model=0 \ num-buffers=$num_buffers ! zedodoverlay ! fakesink 2>&1; then - test_pass "Object Detection pipeline" + test_pass "[OD03] Object Detection pipeline" else - test_fail "Object Detection pipeline" + test_fail "[OD04] Object Detection pipeline" fi } @@ -729,12 +972,12 @@ test_hardware_bt() { print_subheader "Hardware Body Tracking Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Body Tracking pipeline" "No camera detected" + skip_test "[BT01] Body Tracking pipeline" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Body Tracking pipeline" "Extensive mode required" + skip_test "[BT02] Body Tracking pipeline" "Extensive mode required" return 0 fi @@ -743,9 +986,9 @@ test_hardware_bt() { if timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 bt-enabled=true bt-detection-model=0 \ num-buffers=$num_buffers ! zedodoverlay ! fakesink 2>&1; then - test_pass "Body Tracking pipeline" + test_pass "[BT03] Body Tracking pipeline" else - test_fail "Body Tracking pipeline" + test_fail "[BT04] Body Tracking pipeline" fi } @@ -755,14 +998,14 @@ test_csv_sink() { local test_csv="/tmp/zed_gst_test_$$.csv" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "CSV sink with camera data" "No camera detected" + skip_test "[CS01] CSV sink with camera data" "No camera detected" return 2 fi # CSV sink test requires demux which can hang with limited buffers # Only run in extensive mode if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "CSV sink with camera data" "Extensive mode required (uses demux)" + skip_test "[CS02] CSV sink with camera data" "Extensive mode required (uses demux)" return 0 fi @@ -775,14 +1018,13 @@ test_csv_sink() { demux.src_data ! queue ! zeddatacsvsink location="$test_csv" \ demux.src_left ! queue ! fakesink demux.src_aux ! queue ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "CSV sink with camera data" + test_pass "[CS03] CSV sink with camera data" else - test_fail "CSV sink with camera data" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[CS04] CSV sink with camera data" "$output" fi if [ -f "$test_csv" ]; then - test_pass "CSV file was created" + test_pass "[CS05] CSV file was created" rm -f "$test_csv" fi } @@ -792,33 +1034,266 @@ test_zedxone() { # Check if zedxonesrc is available if ! gst-inspect-1.0 zedxonesrc > /dev/null 2>&1; then - skip_test "ZED X One tests" "zedxonesrc not available" + skip_test "[XP01] ZED X One tests" "zedxonesrc not available" return 0 fi if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "camera-resolution"; then - test_pass "zedxonesrc has camera-resolution property" + test_pass "[XP02] zedxonesrc has camera-resolution property" else - test_fail "zedxonesrc has camera-resolution property" + test_fail "[XP03] zedxonesrc has camera-resolution property" fi if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "camera-fps"; then - test_pass "zedxonesrc has camera-fps property" + test_pass "[XP04] zedxonesrc has camera-fps property" else - test_fail "zedxonesrc has camera-fps property" + test_fail "[XP05] zedxonesrc has camera-fps property" fi if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "ctrl-exposure-time"; then - test_pass "zedxonesrc has ctrl-exposure-time property" + test_pass "[XP06] zedxonesrc has ctrl-exposure-time property" else - test_fail "zedxonesrc has ctrl-exposure-time property" + test_fail "[XP07] zedxonesrc has ctrl-exposure-time property" fi if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "stream-type"; then - test_pass "zedxonesrc has stream-type property" + test_pass "[XP08] zedxonesrc has stream-type property" + else + test_fail "[XP09] zedxonesrc has stream-type property" + fi +} + +test_zedxone_enums() { + print_subheader "ZED X One Enum Value Tests" + + # Check if zedxonesrc is available + if ! gst-inspect-1.0 zedxonesrc > /dev/null 2>&1; then + skip_test "[XE01] ZED X One enum tests" "zedxonesrc not available" + return 0 + fi + + # Test resolution enum values — must match xone_camera_modes[] table + local resolutions=("4K" "QHDPLUS" "HD1200" "HD1080" "SVGA") + for res in "${resolutions[@]}"; do + if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "$res"; then + test_pass "[XE02] zedxonesrc has resolution '$res'" + else + test_fail "[XE03] zedxonesrc has resolution '$res'" + fi + done + + # AUTO must also be present + if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "AUTO"; then + test_pass "[XE04] zedxonesrc has resolution 'AUTO'" + else + test_fail "[XE05] zedxonesrc has resolution 'AUTO'" + fi + + # Test stream-type enum values + local stream_types=("Image" "Auto") + for stype in "${stream_types[@]}"; do + if gst-inspect-1.0 zedxonesrc 2>&1 | grep -qi "$stype"; then + test_pass "[XE06] zedxonesrc has stream-type '$stype'" + else + test_fail "[XE07] zedxonesrc has stream-type '$stype'" + fi + done +} + +test_zedxone_auto_resolution() { + print_subheader "ZED X One Auto-Resolution Negotiation Tests" + + # Check if zedxonesrc is available + if ! gst-inspect-1.0 zedxonesrc > /dev/null 2>&1; then + skip_test "[XA01] ZED X One auto-resolution tests" "zedxonesrc not available" + return 0 + fi + + # --- Enum introspection --- + # AUTO resolution enum must be advertised + if gst-inspect-1.0 zedxonesrc 2>&1 | grep -qi "Auto.*best resolution\|AUTO"; then + test_pass "[XA02] zedxonesrc has AUTO resolution enum" + else + test_fail "[XA03] zedxonesrc has AUTO resolution enum" + fi + + # Default camera-resolution must be AUTO + local default_res + default_res=$(gst-inspect-1.0 zedxonesrc 2>&1 | grep -A2 "camera-resolution" | grep -oi "Default.*" | head -1) + if echo "$default_res" | grep -qi "auto\|5"; then + test_pass "[XA04] zedxonesrc default camera-resolution is AUTO" + else + test_fail "[XA05] zedxonesrc default camera-resolution is AUTO (got: $default_res)" + fi + + # Default camera-fps must be 30 + local default_fps + default_fps=$(gst-inspect-1.0 zedxonesrc 2>&1 | grep -A2 "camera-fps" | grep -oi "Default.*" | head -1) + if echo "$default_fps" | grep -q "30"; then + test_pass "[XA06] zedxonesrc default camera-fps is 30" + else + test_fail "[XA07] zedxonesrc default camera-fps is 30 (got: $default_fps)" + fi + + # --- Hardware pipeline tests --- + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[XA08] zedxonesrc AUTO caps inspection" "No camera detected" + return 0 + fi + + # Build camera-sn property for targeting a specific ZED X One camera + local xone_prop="" + if [ -n "$XONE_CAMERA_SN" ]; then + xone_prop="camera-sn=$XONE_CAMERA_SN" + else + skip_test "[XA09] zedxonesrc AUTO caps negotiation" "No ZED X One camera available" + return 0 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[XA10] zedxonesrc AUTO caps negotiation" "Extensive mode required" + return 0 + fi + + local timeout_val=90 + local num_buffers=5 + local output + + # Test 1: Default (AUTO + 30fps) → 4K (3840x2160) on ZED X One + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XA11] zedxonesrc AUTO resolution default pipeline" + else + if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XA12] zedxonesrc AUTO default pipeline" "No ZED X One camera detected" + else + test_fail "[XA13] zedxonesrc AUTO resolution default pipeline" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 2: AUTO + 15fps → should select 4K (highest supporting 15) + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop camera-resolution=5 camera-fps=15 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XA14] zedxonesrc AUTO@15fps pipeline works" + else + if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XA15] zedxonesrc AUTO@15fps pipeline" "No ZED X One camera detected" + else + test_fail "[XA16] zedxonesrc AUTO@15fps pipeline" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 3: AUTO + 60fps → should select QHDPLUS (3200x1800) + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop camera-resolution=5 camera-fps=60 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XA17] zedxonesrc AUTO@60fps pipeline works" + else + if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XA18] zedxonesrc AUTO@60fps pipeline" "No ZED X One camera detected" + else + test_fail "[XA19] zedxonesrc AUTO@60fps pipeline" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 4: AUTO + 120fps → should select QHDPLUS (3200x1800) + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop camera-resolution=5 camera-fps=120 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XA20] zedxonesrc AUTO@120fps pipeline works" + else + if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XA21] zedxonesrc AUTO@120fps pipeline" "No ZED X One camera detected" + else + test_fail "[XA22] zedxonesrc AUTO@120fps pipeline" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 5: Explicit 4K + FPS clamping (4K only supports 15/30 → 60 clamps to 30) + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop camera-resolution=0 camera-fps=60 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XA23] zedxonesrc 4K@60fps clamped to 30fps" + else + if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XA24] zedxonesrc 4K@60fps FPS clamping" "No ZED X One camera detected" + else + test_fail "[XA25] zedxonesrc 4K@60fps FPS clamping" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + + # Test 6: Downstream size negotiation — request smaller output + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop camera-resolution=5 num-buffers=$num_buffers ! \ + "video/x-raw,width=640,height=480" ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XA26] zedxonesrc AUTO with downstream resize (640x480)" else - test_fail "zedxonesrc has stream-type property" + if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XA27] zedxonesrc AUTO downstream resize" "No ZED X One camera detected" + else + test_fail "[XA28] zedxonesrc AUTO with downstream resize (640x480)" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY +} + +test_zedxone_resolutions() { + print_subheader "Resolution Tests — all zedxonesrc mode table entries (requires ZED X One)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[XR01] ZED X One resolution tests" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[XR02] ZED X One resolution tests" "Extensive mode required" + return 0 fi + + local xone_prop="" + [ -n "$XONE_CAMERA_SN" ] && xone_prop="camera-sn=$XONE_CAMERA_SN" + + local timeout_val=90 + local num_buffers=5 + local output + + # Iterate every entry in the xone_camera_modes[] mapping table. + # Each line: enum_value label WxH + local modes=( + "4 4K 3840x2160" + "3 QHDPLUS 3200x1800" + "2 HD1200 1920x1200" + "1 HD1080 1920x1080" + "0 SVGA 960x600" + ) + + for entry in "${modes[@]}"; do + local enum_val name dims + read -r enum_val name dims <<< "$entry" + + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop camera-resolution=$enum_val num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[XR03] zedxonesrc $name ($dims) [enum=$enum_val]" + else + if echo "$output" | grep -qi "invalid resolution\|not available\|not supported"; then + skip_test "[XR04] zedxonesrc $name ($dims)" "Resolution not supported by connected camera model" + elif echo "$output" | grep -qi "no camera\|not found\|failed to open"; then + skip_test "[XR05] zedxonesrc $name ($dims)" "No ZED X One camera detected" + else + test_fail "[XR06] zedxonesrc $name ($dims) [enum=$enum_val]" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY + done } test_zedxone_nv12() { @@ -826,7 +1301,7 @@ test_zedxone_nv12() { # Check if zedxonesrc is available if ! gst-inspect-1.0 zedxonesrc > /dev/null 2>&1; then - skip_test "ZED X One NV12 tests" "zedxonesrc not available" + skip_test "[XN01] ZED X One NV12 tests" "zedxonesrc not available" return 0 fi @@ -834,37 +1309,37 @@ test_zedxone_nv12() { local zedxone_zerocopy_available=false if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "Raw NV12 zero-copy"; then zedxone_zerocopy_available=true - test_pass "zedxonesrc supports NV12 zero-copy (stream-type=1)" + test_pass "[XN02] zedxonesrc supports NV12 zero-copy (stream-type=1)" else - skip_test "zedxonesrc NV12 zero-copy" "SDK < 5.2 or not built with Advanced Capture API" + skip_test "[XN03] zedxonesrc NV12 zero-copy" "SDK < 5.2 or not built with Advanced Capture API" fi # Check for AUTO stream type if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "Auto.*NV12 zero-copy"; then - test_pass "zedxonesrc has STREAM_AUTO with NV12 preference" + test_pass "[XN04] zedxonesrc has STREAM_AUTO with NV12 preference" else # May not have zero-copy but should still have AUTO if gst-inspect-1.0 zedxonesrc 2>&1 | grep -q "Auto-negotiate"; then - test_pass "zedxonesrc has STREAM_AUTO" + test_pass "[XN05] zedxonesrc has STREAM_AUTO" else - test_fail "zedxonesrc has STREAM_AUTO" + test_fail "[XN06] zedxonesrc has STREAM_AUTO" fi fi # Hardware test for NV12 zero-copy if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "ZED X One NV12 hardware test" "No camera detected" + skip_test "[XN07] ZED X One NV12 hardware test" "No camera detected" return 2 fi # Only test on Jetson with zero-copy available if [ ! -f /etc/nv_tegra_release ]; then - skip_test "ZED X One NV12 hardware test" "Not a Jetson platform" + skip_test "[XN08] ZED X One NV12 hardware test" "Not a Jetson platform" return 0 fi if [ "$zedxone_zerocopy_available" = false ]; then - skip_test "ZED X One NV12 hardware test" "Zero-copy not available" + skip_test "[XN09] ZED X One NV12 hardware test" "Zero-copy not available" return 0 fi @@ -874,25 +1349,27 @@ test_zedxone_nv12() { local timeout_val=30 local num_buffers=10 + local xone_prop="" + [ -n "$XONE_CAMERA_SN" ] && xone_prop="camera-sn=$XONE_CAMERA_SN" + # Test stream-type=1 (NV12 zero-copy for ZED X One) local output - output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc stream-type=1 num-buffers=$num_buffers ! \ + output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc $xone_prop stream-type=1 num-buffers=$num_buffers ! \ 'video/x-raw(memory:NVMM),format=NV12' ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "ZED X One NV12 zero-copy pipeline (stream-type=1)" + test_pass "[XN10] ZED X One NV12 zero-copy pipeline (stream-type=1)" else # May fail if no ZED X One camera is connected (which is OK) if echo "$output" | grep -qi "no camera\|not found\|failed to open"; then - skip_test "ZED X One NV12 zero-copy pipeline" "No ZED X One camera detected" + skip_test "[XN11] ZED X One NV12 zero-copy pipeline" "No ZED X One camera detected" else - test_fail "ZED X One NV12 zero-copy pipeline (stream-type=1)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -3 + test_fail "[XN12] ZED X One NV12 zero-copy pipeline (stream-type=1)" "$output" fi fi sleep $CAMERA_RESET_DELAY else - skip_test "ZED X One NV12 hardware test" "Extensive mode required" + skip_test "[XN13] ZED X One NV12 hardware test" "Extensive mode required" fi } @@ -900,12 +1377,12 @@ test_latency_query() { print_subheader "Latency Query Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "zedsrc latency query" "No camera detected" + skip_test "[LA01] zedsrc latency query" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "zedsrc latency query" "Extensive mode required" + skip_test "[LA02] zedsrc latency query" "Extensive mode required" return 0 fi @@ -917,10 +1394,9 @@ test_latency_query() { output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc num-buffers=30 ! \ fakesink sync=true 2>&1) if [ $? -eq 0 ]; then - test_pass "zedsrc with synchronized sink" + test_pass "[LA03] zedsrc with synchronized sink" else - test_fail "zedsrc with synchronized sink" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|latency" | head -3 + test_fail "[LA04] zedsrc with synchronized sink" "$output" fi sleep $CAMERA_RESET_DELAY @@ -930,12 +1406,12 @@ test_buffer_metadata() { print_subheader "Buffer Metadata Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Buffer timestamps working" "No camera detected" + skip_test "[TS01] Buffer timestamps working" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Buffer timestamps working" "Extensive mode required" + skip_test "[TS02] Buffer timestamps working" "Extensive mode required" return 0 fi @@ -949,10 +1425,9 @@ test_buffer_metadata() { queue max-size-buffers=5 ! fakesink sync=true 2>&1) if [ $? -eq 0 ]; then - test_pass "Buffer timestamps with queued sync sink" + test_pass "[TS03] Buffer timestamps with queued sync sink" else - test_fail "Buffer timestamps with queued sync sink" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[TS04] Buffer timestamps with queued sync sink" "$output" fi sleep $CAMERA_RESET_DELAY @@ -962,12 +1437,12 @@ test_datamux() { print_subheader "Data Mux Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "zeddatamux pipeline" "No camera detected" + skip_test "[MX01] zeddatamux pipeline" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "zeddatamux pipeline" "Extensive mode required" + skip_test "[MX02] zeddatamux pipeline" "Extensive mode required" return 0 fi @@ -986,10 +1461,9 @@ test_datamux() { demux.src_aux ! queue ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "zeddatamux demux/mux round-trip" + test_pass "[MX03] zeddatamux demux/mux round-trip" else - test_fail "zeddatamux demux/mux round-trip" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error\|fail" | head -5 + test_fail "[MX04] zeddatamux demux/mux round-trip" "$output" fi sleep $CAMERA_RESET_DELAY @@ -999,12 +1473,12 @@ test_overlay_skeletons() { print_subheader "Overlay Skeleton Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "zedodoverlay with body tracking" "No camera detected" + skip_test "[OV01] zedodoverlay with body tracking" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "zedodoverlay with body tracking" "Extensive mode required" + skip_test "[OV02] zedodoverlay with body tracking" "Extensive mode required" return 0 fi @@ -1020,13 +1494,13 @@ test_overlay_skeletons() { bt-enabled=true bt-format=0 num-buffers=$num_buffers ! \ zedodoverlay ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "zedodoverlay with BODY_18 skeleton" + test_pass "[OV03] zedodoverlay with BODY_18 skeleton" else # Body tracking may fail without a person in view, that's OK if echo "$output" | grep -qi "skeleton"; then - test_fail "zedodoverlay with BODY_18 skeleton" + test_fail "[OV04] zedodoverlay with BODY_18 skeleton" else - test_pass "zedodoverlay with BODY_18 skeleton (no bodies detected)" + test_pass "[OV05] zedodoverlay with BODY_18 skeleton (no bodies detected)" fi fi @@ -1037,12 +1511,12 @@ test_overlay_skeletons() { bt-enabled=true bt-format=2 num-buffers=$num_buffers ! \ zedodoverlay ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "zedodoverlay with BODY_38 skeleton" + test_pass "[OV06] zedodoverlay with BODY_38 skeleton" else if echo "$output" | grep -qi "skeleton"; then - test_fail "zedodoverlay with BODY_38 skeleton" + test_fail "[OV07] zedodoverlay with BODY_38 skeleton" else - test_pass "zedodoverlay with BODY_38 skeleton (no bodies detected)" + test_pass "[OV08] zedodoverlay with BODY_38 skeleton (no bodies detected)" fi fi @@ -1050,84 +1524,65 @@ test_overlay_skeletons() { } test_resolutions() { - print_subheader "Resolution Tests (requires camera)" - + print_subheader "Resolution Tests — all zedsrc mode table entries (requires camera)" + if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Resolution HD1080" "No camera detected" - skip_test "Resolution HD720/HD1200" "No camera detected" - skip_test "Resolution SVGA" "No camera detected" + skip_test "[RS01] Resolution tests" "No camera detected" return 2 fi - + if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Resolution tests" "Extensive mode required" + skip_test "[RS02] Resolution tests" "Extensive mode required" return 0 fi - + local timeout_val=90 local num_buffers=10 local output - - # Use global GMSL_CAMERA variable from check_hardware() - # Test HD1080 resolution (1920x1080) - supported by ALL cameras - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=1 num-buffers=$num_buffers ! fakesink 2>&1) - if [ $? -eq 0 ]; then - test_pass "Resolution HD1080 (1920x1080)" - else - test_fail "Resolution HD1080 (1920x1080)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 - fi - - sleep $CAMERA_RESET_DELAY - - # Test camera-specific resolution - if [ "$GMSL_CAMERA" = true ]; then - # GMSL cameras (ZED X, ZED X Mini): Test HD1200 (1920x1200) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=2 num-buffers=$num_buffers ! fakesink 2>&1) - if [ $? -eq 0 ]; then - test_pass "Resolution HD1200 (1920x1200) [GMSL]" - else - test_fail "Resolution HD1200 (1920x1200) [GMSL]" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 - fi - else - # USB cameras (ZED, ZED 2, ZED 2i): Test HD720 (1280x720) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=3 num-buffers=$num_buffers ! fakesink 2>&1) + # Iterate every entry in the zed_camera_modes[] mapping table. + # Each line: enum_value label WxH + # Dimensions come from sl::getResolution() at runtime; shown here for reference. + local modes=( + "0 HD2K 2208x1242" + "1 HD1080 1920x1080" + "2 HD1200 1920x1200" + "3 HD720 1280x720" + "4 SVGA 960x600" + "5 VGA 672x376" + ) + + for entry in "${modes[@]}"; do + local enum_val name dims + read -r enum_val name dims <<< "$entry" + + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=$enum_val num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Resolution HD720 (1280x720) [USB]" + test_pass "[RS03] zedsrc $name ($dims) [enum=$enum_val]" else - test_fail "Resolution HD720 (1280x720) [USB]" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + # Resolution may be unavailable for this camera model — skip rather than fail + if echo "$output" | grep -qi "not available\|not supported\|invalid resolution\|failed to open"; then + skip_test "[RS04] zedsrc $name ($dims)" "Not supported on this camera" + else + test_fail "[RS05] zedsrc $name ($dims) [enum=$enum_val]" "$output" + fi fi - fi - - sleep $CAMERA_RESET_DELAY - - # Test SVGA resolution (960x600) - exercises the VGA->SVGA mapping fix - # On USB cameras this maps to VGA (672x376) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-resolution=4 num-buffers=$num_buffers ! fakesink 2>&1) - if [ $? -eq 0 ]; then - test_pass "Resolution SVGA/VGA (low-res mode)" - else - test_fail "Resolution SVGA/VGA (low-res mode)" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 - fi - - sleep $CAMERA_RESET_DELAY + + sleep $CAMERA_RESET_DELAY + done } test_depth_modes() { print_subheader "Depth Mode Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Depth mode NEURAL" "No camera detected" - skip_test "Depth mode ULTRA" "No camera detected" + skip_test "[DP01] Depth mode NEURAL" "No camera detected" + skip_test "[DP02] Depth mode ULTRA" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Depth mode tests" "Extensive mode required" + skip_test "[DP03] Depth mode tests" "Extensive mode required" return 0 fi @@ -1138,10 +1593,9 @@ test_depth_modes() { # Test NEURAL depth mode (mode 4) - most computationally intensive output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=4 depth-mode=4 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Depth mode NEURAL" + test_pass "[DP04] Depth mode NEURAL" else - test_fail "Depth mode NEURAL" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[DP05] Depth mode NEURAL" "$output" fi sleep $CAMERA_RESET_DELAY @@ -1149,10 +1603,9 @@ test_depth_modes() { # Test ULTRA depth mode (mode 3) output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=4 depth-mode=3 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Depth mode ULTRA" + test_pass "[DP06] Depth mode ULTRA" else - test_fail "Depth mode ULTRA" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[DP07] Depth mode ULTRA" "$output" fi sleep $CAMERA_RESET_DELAY @@ -1162,12 +1615,12 @@ test_positional_tracking() { print_subheader "Positional Tracking Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Positional tracking enabled" "No camera detected" + skip_test "[PK01] Positional tracking enabled" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Positional tracking tests" "Extensive mode required" + skip_test "[PK02] Positional tracking tests" "Extensive mode required" return 0 fi @@ -1179,10 +1632,9 @@ test_positional_tracking() { output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 \ enable-positional-tracking=true depth-mode=1 num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Positional tracking enabled" + test_pass "[PK03] Positional tracking enabled" else - test_fail "Positional tracking enabled" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[PK04] Positional tracking enabled" "$output" fi sleep $CAMERA_RESET_DELAY @@ -1192,10 +1644,9 @@ test_positional_tracking() { enable-positional-tracking=true enable-area-memory=true depth-mode=1 \ num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Positional tracking with area memory" + test_pass "[PK05] Positional tracking with area memory" else - test_fail "Positional tracking with area memory" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[PK06] Positional tracking with area memory" "$output" fi sleep $CAMERA_RESET_DELAY @@ -1205,23 +1656,25 @@ test_zedxone_hardware() { print_subheader "ZED X One Hardware Tests (requires ZED X One camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "ZED X One camera grab" "No camera detected" + skip_test "[XH01] ZED X One camera grab" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "ZED X One hardware tests" "Extensive mode required" + skip_test "[XH02] ZED X One hardware tests" "Extensive mode required" return 0 fi - # Check if ZED X One is specifically available - local zed_output - zed_output=$(ZED_Explorer -a 2>&1) - if ! echo "$zed_output" | grep -qi "ZED X One"; then - skip_test "ZED X One camera grab" "No ZED X One camera detected" - skip_test "ZED X One with HDR" "No ZED X One camera detected" + # Check if ZED X One is specifically available using a quick pipeline probe. + # ZED_Explorer output format varies across SDK versions, so this is more + # reliable than string matching. + if ! timeout 15 gst-launch-1.0 zedxonesrc num-buffers=1 ! fakesink &>/dev/null; then + skip_test "[XH03] ZED X One camera grab" "No ZED X One camera available" + skip_test "[XH04] ZED X One with HDR" "No ZED X One camera available" + sleep $CAMERA_RESET_DELAY return 0 fi + sleep $CAMERA_RESET_DELAY local timeout_val=90 local num_buffers=10 @@ -1230,21 +1683,23 @@ test_zedxone_hardware() { # Test basic ZED X One grab output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "ZED X One camera grab" + test_pass "[XH05] ZED X One camera grab" else - test_fail "ZED X One camera grab" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[XH06] ZED X One camera grab" "$output" fi sleep $CAMERA_RESET_DELAY - # Test ZED X One with HDR enabled + # Test ZED X One with HDR enabled (requires ZED X One HDR — ISX031 sensor) output=$(timeout "$timeout_val" gst-launch-1.0 zedxonesrc enable-hdr=true num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "ZED X One with HDR" + test_pass "[XH07] ZED X One with HDR" else - test_fail "ZED X One with HDR" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + if echo "$output" | grep -qi "not compatible with.*hdr\|invalid function call\|hdr.*not supported\|invalid resolution"; then + skip_test "[XH08] ZED X One with HDR" "HDR requires ZED X One HDR model (ISX031 sensor)" + else + test_fail "[XH09] ZED X One with HDR" "$output" + fi fi sleep $CAMERA_RESET_DELAY @@ -1254,13 +1709,13 @@ test_camera_controls() { print_subheader "Camera Control Tests (requires camera)" if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Camera exposure control" "No camera detected" - skip_test "Camera gain control" "No camera detected" + skip_test "[CC01] Camera exposure control" "No camera detected" + skip_test "[CC02] Camera gain control" "No camera detected" return 2 fi if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Camera control tests" "Extensive mode required" + skip_test "[CC03] Camera control tests" "Extensive mode required" return 0 fi @@ -1273,10 +1728,9 @@ test_camera_controls() { ctrl-aec-agc=false ctrl-exposure=5000 ctrl-gain=50 \ num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Camera manual exposure/gain control" + test_pass "[CC04] Camera manual exposure/gain control" else - test_fail "Camera manual exposure/gain control" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[CC05] Camera manual exposure/gain control" "$output" fi sleep $CAMERA_RESET_DELAY @@ -1287,314 +1741,1246 @@ test_camera_controls() { ctrl-aec-agc-roi-w=200 ctrl-aec-agc-roi-h=200 \ num-buffers=$num_buffers ! fakesink 2>&1) if [ $? -eq 0 ]; then - test_pass "Camera auto exposure with ROI" + test_pass "[CC06] Camera auto exposure with ROI" + else + test_fail "[CC07] Camera auto exposure with ROI" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # Test white balance control + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc \ + ctrl-whitebalance-auto=false ctrl-whitebalance-temperature=4500 \ + num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[CC08] Camera white balance control" + else + test_fail "[CC09] Camera white balance control" "$output" + fi + + sleep $CAMERA_RESET_DELAY +} + +test_video_recording_playback() { + print_subheader "Video Recording/Playback Tests" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[VR01] Video recording" "No camera detected" + skip_test "[VR02] Video playback" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[VR03] Video recording/playback tests" "Extensive mode required" + return 0 + fi + + local timeout_val=90 + local test_dir="/tmp/zed_gst_test_$$" + local video_file="$test_dir/test_recording.mp4" + local output + + # Create test directory + mkdir -p "$test_dir" + + # STEP 1: Record video from camera to file + build_h264_encode_pipeline encode + if [ "$_ENCODE_AVAILABLE" != true ]; then + skip_test "[VR04] Video recording" "No H.264 encoder available" + skip_test "[VR05] Video playback" "No H.264 encoder available" + rm -rf "$test_dir" + return 0 + fi + + local st="$_ENCODE_STREAM_TYPE" + local encoder="$_ENCODE_PIPELINE" + local encoder_name="$_ENCODER_NAME" + local cam_prop="$_ENCODE_CAMERA_PROP" + + # Use mp4mux for a standard container format + # shellcheck disable=SC2086 + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=$st $cam_prop num-buffers=60 ! \ + queue ! $encoder ! h264parse ! \ + mp4mux ! filesink location=$video_file 2>&1) + local record_status=$? + + if [ $record_status -eq 0 ] && [ -f "$video_file" ]; then + local file_size + file_size=$(stat -c%s "$video_file" 2>/dev/null || echo "0") + if [ "$file_size" -gt 1000 ]; then + test_pass "[VR06] Video recording to MP4 ($file_size bytes, $encoder_name)" + else + test_fail "[VR07] Video recording (file too small: $file_size bytes)" + rm -rf "$test_dir" + return 1 + fi + else + test_fail "[VR08] Video recording to MP4 ($encoder_name)" "$output" + rm -rf "$test_dir" + return 1 + fi + + sleep 1 + + # STEP 2: Playback the recorded file + output=$(timeout 30 gst-launch-1.0 filesrc location="$video_file" ! \ + qtdemux ! h264parse ! avdec_h264 ! videoconvert ! fakesink 2>&1) + local playback_status=$? + + if [ $playback_status -eq 0 ]; then + test_pass "[VR09] Video playback from recorded file" + else + test_fail "[VR10] Video playback from recorded file" "$output" + fi + + # Cleanup + rm -rf "$test_dir" + + sleep $CAMERA_RESET_DELAY +} + +test_udp_streaming() { + print_subheader "UDP Streaming Tests (sender/receiver)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[US01] UDP stream sender" "No camera detected" + skip_test "[US02] UDP stream receiver" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[US03] UDP streaming tests" "Extensive mode required" + return 0 + fi + + local timeout_val=30 + local stream_port=5000 + local num_buffers=90 # ~3 seconds at 30fps + local output + + # Find an available port + while netstat -tuln 2>/dev/null | grep -q ":$stream_port " || \ + ss -tuln 2>/dev/null | grep -q ":$stream_port "; do + stream_port=$((stream_port + 1)) + if [ $stream_port -gt 5100 ]; then + skip_test "[US04] UDP streaming" "No available port found" + return 0 + fi + done + + # Start receiver in background first (must be ready before sender) + local receiver_output="/tmp/zed_udp_receiver_$$.log" + timeout "$timeout_val" gst-launch-1.0 \ + udpsrc port=$stream_port caps="application/x-rtp,media=video,encoding-name=H264" ! \ + rtph264depay ! h264parse ! avdec_h264 ! fakesink sync=false \ + > "$receiver_output" 2>&1 & + local receiver_pid=$! + + # Give receiver time to bind + sleep 2 + + # Check if receiver is still running + if ! kill -0 $receiver_pid 2>/dev/null; then + test_fail "[US05] UDP receiver failed to start" + rm -f "$receiver_output" + return 1 + fi + + # Build optimal sender pipeline via shared helper + build_h264_encode_pipeline encode + if [ "$_ENCODE_AVAILABLE" != true ]; then + kill $receiver_pid 2>/dev/null + wait $receiver_pid 2>/dev/null + skip_test "[US06] UDP stream sender" "No H.264 encoder available" + skip_test "[US07] UDP stream receiver" "No H.264 encoder available" + rm -f "$receiver_output" + return 0 + fi + + local st="$_ENCODE_STREAM_TYPE" + local encoder="$_ENCODE_PIPELINE" + local encoder_name="$_ENCODER_NAME" + local cam_prop="$_ENCODE_CAMERA_PROP" + + # Start sender - stream camera to UDP + # shellcheck disable=SC2086 + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=$st $cam_prop num-buffers=$num_buffers ! \ + queue ! $encoder ! \ + rtph264pay ! udpsink host=127.0.0.1 port=$stream_port 2>&1) + local sender_status=$? + + # Give receiver a moment to process + sleep 1 + + # Stop receiver + kill $receiver_pid 2>/dev/null + wait $receiver_pid 2>/dev/null + + # Check results + if [ $sender_status -eq 0 ]; then + # Check if receiver got any data + if [ -f "$receiver_output" ]; then + local receiver_log + receiver_log=$(cat "$receiver_output") + if echo "$receiver_log" | grep -qi "PLAYING\|pipeline\|clock"; then + test_pass "[US08] UDP stream sender ($encoder_name, port $stream_port)" + test_pass "[US09] UDP stream receiver (verified data flow)" + else + test_pass "[US10] UDP stream sender ($encoder_name, port $stream_port)" + test_fail "[US11] UDP stream receiver (no data received)" + fi + else + test_pass "[US12] UDP stream sender ($encoder_name, port $stream_port)" + skip_test "[US13] UDP stream receiver" "Could not verify" + fi + else + test_fail "[US14] UDP stream sender ($encoder_name)" + test_fail "[US15] UDP stream receiver" "$output" + fi + + rm -f "$receiver_output" + sleep $CAMERA_RESET_DELAY +} + +test_network_streaming() { + print_subheader "Network Streaming Tests" + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[NS01] Network streaming tests" "Extensive mode required" + return 0 + fi + + local timeout_val=10 + local exit_code + local output + + # Test connecting to a non-existent stream (should fail or timeout) + # This tests error handling for invalid stream input + # Using a TEST-NET address (RFC 5737) that is guaranteed not to route + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc input-stream-ip="192.0.2.1" input-stream-port=30000 ! fakesink 2>&1) + exit_code=$? + + # Exit code 124 = timeout, non-zero = error - both are acceptable failures + if [ $exit_code -ne 0 ]; then + test_pass "[NS02] Network stream invalid IP (failed as expected, exit code: $exit_code)" + else + # Unexpectedly succeeded - this shouldn't happen with an invalid IP + test_fail "[NS03] Network stream invalid IP (should have failed)" + fi +} + +test_error_handling() { + print_subheader "Error Handling Tests" + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[EH01] Error handling tests" "Extensive mode required" + return 0 + fi + + local timeout_val=15 + local output + + # Test invalid camera ID (should fail gracefully) + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-id=99 ! fakesink 2>&1) + if [ $? -ne 0 ]; then + if echo "$output" | grep -qi "error\|failed\|not found\|open"; then + test_pass "[EH02] Invalid camera ID (graceful failure)" + else + test_fail "[EH03] Invalid camera ID (unexpected failure mode)" + fi + else + test_fail "[EH04] Invalid camera ID (should have failed)" + fi + + # Test invalid serial number (should fail gracefully) + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-sn=999999999 ! fakesink 2>&1) + if [ $? -ne 0 ]; then + if echo "$output" | grep -qi "error\|failed\|not found\|open"; then + test_pass "[EH05] Invalid serial number (graceful failure)" + else + test_fail "[EH06] Invalid serial number (unexpected failure mode)" + fi + else + test_fail "[EH07] Invalid serial number (should have failed)" + fi + + # Test invalid SVO file path (should fail gracefully) + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc svo-file="/nonexistent/path/video.svo" ! fakesink 2>&1) + if [ $? -ne 0 ]; then + # Any failure is acceptable - the key is it doesn't hang or crash + test_pass "[EH08] Invalid SVO file path (graceful failure)" + else + test_fail "[EH09] Invalid SVO file path (should have failed)" + fi + + # Test CSV sink with invalid location (should fail gracefully) + output=$(timeout "$timeout_val" gst-launch-1.0 videotestsrc num-buffers=1 ! \ + "application/data" ! zeddatacsvsink location="/nonexistent/dir/test.csv" 2>&1) + if [ $? -ne 0 ]; then + # Any failure is acceptable + test_pass "[EH10] CSV sink invalid path (graceful failure)" + else + test_fail "[EH11] CSV sink invalid path (should have failed)" + fi + + # Test zedodoverlay with non-video input (should fail or handle gracefully) + output=$(timeout "$timeout_val" gst-launch-1.0 audiotestsrc num-buffers=1 ! \ + zedodoverlay ! fakesink 2>&1) + if [ $? -ne 0 ]; then + # Any failure is acceptable - caps negotiation failure is expected + test_pass "[EH12] Overlay with invalid input (graceful failure)" + else + test_fail "[EH13] Overlay with invalid input (should have failed)" + fi +} + +# ============================================================================= +# Lifecycle & Reconnection Regression Tests +# ============================================================================= +# These tests target bugs fixed in the RTSP-reconnection / buffer-ownership +# patch set. They exercise stop→start cycling, element teardown, and +# RTSP client reconnection to make sure the camera is properly released +# and re-acquired each time. + +# Helper: run a GStreamer pipeline N times in a row, with a camera-reset +# delay between runs. Returns 0 only if ALL iterations succeed. +run_pipeline_cycles() { + local label="$1" + local iterations="$2" + local timeout_val="$3" + shift 3 + # remaining args = the gst-launch-1.0 arguments + + local i + for i in $(seq 1 "$iterations"); do + local output + output=$(timeout "$timeout_val" gst-launch-1.0 "$@" 2>&1) + if [ $? -ne 0 ]; then + test_fail "$label (iteration $i/$iterations)" "$output" + return 1 + fi + log_verbose "$label: iteration $i/$iterations OK" + [ "$i" -lt "$iterations" ] && sleep "$CAMERA_RESET_DELAY" + done + test_pass "$label ($iterations cycles)" + return 0 +} + +test_source_stop_start() { + print_subheader "Source Stop/Start Cycling (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[SS01] zedsrc stop/start cycle" "No camera detected" + skip_test "[SS02] zedsrc AUTO stream-type re-negotiation" "No camera detected" + return 2 + fi + + local iterations=3 + local num_buffers=5 + local timeout_val=$FAST_PIPELINE_TIMEOUT + + if [ "$EXTENSIVE_MODE" = true ]; then + iterations=5 + num_buffers=10 + timeout_val=$EXTENSIVE_PIPELINE_TIMEOUT + fi + + # --- zedsrc stop/start --- + # Regression: resolved_stream_type and stop_requested were not reset, + # causing the second pipeline start to fail. + run_pipeline_cycles \ + "[SS03] zedsrc stop/start cycle" "$iterations" "$timeout_val" \ + zedsrc stream-type=0 num-buffers=$num_buffers ! fakesink + + sleep $CAMERA_RESET_DELAY + + # --- AUTO stream-type re-negotiation after explicit type --- + # Start with an explicit stream-type, then switch to AUTO. + # If resolved_stream_type is not cleared, AUTO will keep the old type. + local output + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=2 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -ne 0 ]; then + test_fail "[SS04] zedsrc AUTO re-negotiation: explicit run failed" "$output" + else + sleep $CAMERA_RESET_DELAY + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=2 num-buffers=$num_buffers ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[SS05] zedsrc AUTO stream-type re-negotiation" + else + test_fail "[SS06] zedsrc AUTO stream-type re-negotiation" "$output" + fi + fi + + sleep $CAMERA_RESET_DELAY +} + +test_zedxone_stop_start() { + print_subheader "ZED X One Stop/Start Cycling (requires ZED X One camera)" + + # Detect ZED X One camera via gst-inspect + local has_xone=false + if gst-inspect-1.0 zedxonesrc &>/dev/null && [ "$GMSL_CAMERA" = true ]; then + has_xone=true + fi + + if [ "$HARDWARE_AVAILABLE" = false ] || [ "$has_xone" = false ]; then + skip_test "[XS01] zedxonesrc stop/start cycle" "No ZED X One camera detected" + return 2 + fi + + local iterations=3 + local num_buffers=5 + local timeout_val=$FAST_PIPELINE_TIMEOUT + + if [ "$EXTENSIVE_MODE" = true ]; then + iterations=5 + num_buffers=10 + timeout_val=$EXTENSIVE_PIPELINE_TIMEOUT + fi + + run_pipeline_cycles \ + "[XS02] zedxonesrc stop/start cycle" "$iterations" "$timeout_val" \ + zedxonesrc num-buffers=$num_buffers ! fakesink + + sleep $CAMERA_RESET_DELAY +} + +test_demux_lifecycle() { + print_subheader "Demux Lifecycle Tests (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[DL01] Demux stop/start cycle" "No camera detected" + skip_test "[DL02] Demux teardown (no leak/crash)" "No camera detected" + return 2 + fi + + local iterations=3 + local num_buffers=10 + local timeout_val=90 + + if [ "$EXTENSIVE_MODE" = true ]; then + iterations=4 + num_buffers=20 + fi + + # Regression: missing finalize leaked caps; missing change_state left stale + # caps on restart; double-free after gst_pad_push crashed on teardown. + local i + local all_ok=true + for i in $(seq 1 "$iterations"); do + local output + output=$(timeout "$timeout_val" gst-launch-1.0 \ + zedsrc stream-type=2 num-buffers=$num_buffers ! \ + zeddemux is-depth=false name=demux \ + demux.src_left ! queue ! fakesink \ + demux.src_aux ! queue ! fakesink 2>&1) + if [ $? -ne 0 ]; then + test_fail "[DL03] Demux stop/start cycle (iteration $i/$iterations)" "$output" + all_ok=false + break + fi + log_verbose "Demux cycle $i/$iterations OK" + [ "$i" -lt "$iterations" ] && sleep "$CAMERA_RESET_DELAY" + done + [ "$all_ok" = true ] && test_pass "[DL03] Demux stop/start cycle ($iterations cycles)" + + sleep $CAMERA_RESET_DELAY + + # Quick teardown test: create and destroy immediately + local output + output=$(timeout 30 gst-launch-1.0 \ + zedsrc stream-type=2 num-buffers=2 ! \ + zeddemux is-depth=false stream-data=true name=demux \ + demux.src_left ! queue ! fakesink \ + demux.src_aux ! queue ! fakesink \ + demux.src_data ! queue ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[DL04] Demux teardown (no leak/crash)" + else + test_fail "[DL05] Demux teardown (no leak/crash)" "$output" + fi + + sleep $CAMERA_RESET_DELAY +} + +test_datamux_lifecycle() { + print_subheader "Data-Mux Lifecycle Tests (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[ML01] Data-mux stop/start cycle" "No camera detected" + skip_test "[ML02] Data-mux rapid teardown" "No camera detected" + return 2 + fi + + local iterations=3 + local num_buffers=10 + local timeout_val=90 + + if [ "$EXTENSIVE_MODE" = true ]; then + iterations=4 + num_buffers=20 + fi + + # Regression: buffer use-after-free in chain_data/chain_video when + # gst_pad_push transferred ownership but code still touched the buffer. + local i + local all_ok=true + for i in $(seq 1 "$iterations"); do + local output + output=$(timeout "$timeout_val" gst-launch-1.0 \ + zedsrc stream-type=2 num-buffers=$num_buffers ! \ + zeddemux stream-data=true name=demux \ + demux.src_left ! queue ! zeddatamux name=mux ! queue ! fakesink \ + demux.src_data ! queue ! mux.sink_data \ + demux.src_aux ! queue ! fakesink 2>&1) + if [ $? -ne 0 ]; then + test_fail "[ML03] Data-mux stop/start cycle (iteration $i/$iterations)" "$output" + all_ok=false + break + fi + log_verbose "Data-mux cycle $i/$iterations OK" + [ "$i" -lt "$iterations" ] && sleep "$CAMERA_RESET_DELAY" + done + [ "$all_ok" = true ] && test_pass "[ML03] Data-mux stop/start cycle ($iterations cycles)" + + sleep $CAMERA_RESET_DELAY + + # Rapid teardown: very few buffers so GStreamer tears the pipeline + # apart quickly — stresses finalize + change_state paths. + local output + output=$(timeout 30 gst-launch-1.0 \ + zedsrc stream-type=2 num-buffers=2 ! \ + zeddemux stream-data=true name=demux \ + demux.src_left ! queue ! zeddatamux name=mux ! queue ! fakesink \ + demux.src_data ! queue ! mux.sink_data \ + demux.src_aux ! queue ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[ML04] Data-mux rapid teardown" + else + test_fail "[ML05] Data-mux rapid teardown" "$output" + fi + + sleep $CAMERA_RESET_DELAY +} + +test_rtsp_reconnection() { + print_subheader "RTSP Server Reconnection Tests (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[RR01] RTSP server reconnection" "No camera detected" + skip_test "[RR02] RTSP multi-client" "No camera detected" + return 2 + fi + + if ! command -v gst-zed-rtsp-launch &>/dev/null; then + skip_test "[RR03] RTSP server reconnection" "gst-zed-rtsp-launch not installed" + skip_test "[RR04] RTSP multi-client" "gst-zed-rtsp-launch not installed" + return 2 + fi + + # We need rtspsrc or gst-play-1.0 as a client + if ! gst-inspect-1.0 rtspsrc &>/dev/null; then + skip_test "[RR05] RTSP server reconnection" "rtspsrc element not available" + skip_test "[RR06] RTSP multi-client" "rtspsrc element not available" + return 2 + fi + + local rtsp_port=8554 + local rtsp_url="rtsp://127.0.0.1:${rtsp_port}/zed-stream" + local server_log="/tmp/zed_rtsp_server_$$.log" + local client_log="/tmp/zed_rtsp_client_$$.log" + local server_pid="" + local timeout_client=20 + local connect_cycles=3 + + if [ "$EXTENSIVE_MODE" = true ]; then + connect_cycles=5 + fi + + # Find a free port + while ss -tuln 2>/dev/null | grep -q ":$rtsp_port "; do + rtsp_port=$((rtsp_port + 1)) + rtsp_url="rtsp://127.0.0.1:${rtsp_port}/zed-stream" + if [ $rtsp_port -gt 8600 ]; then + skip_test "[RR07] RTSP server reconnection" "No available port" + return 0 + fi + done + + # Build the RTSP server pipeline via the shared helper. + build_h264_encode_pipeline rtsp + if [ "$_ENCODE_AVAILABLE" != true ]; then + skip_test "[RR08] RTSP server reconnection" "No H.264 encoder available" + skip_test "[RR09] RTSP multi-client" "No H.264 encoder available" + return 0 + fi + local rtsp_pipeline="zedsrc stream-type=$_ENCODE_STREAM_TYPE $_ENCODE_CAMERA_PROP ! queue ! $_ENCODE_PIPELINE" + + # Start RTSP server in background. + # NOTE: gst-zed-rtsp-launch uses gst_parse_launchv(), which expects + # the pipeline elements as separate argv words. We intentionally + # leave $rtsp_pipeline UNQUOTED so bash performs word-splitting. + # The parentheses in "video/x-raw(memory:NVMM)" are safe because + # bash does not interpret parentheses during variable expansion. + # shellcheck disable=SC2086 + gst-zed-rtsp-launch -p "$rtsp_port" $rtsp_pipeline > "$server_log" 2>&1 & + server_pid=$! + + # Wait for server to be ready + sleep 5 + + if ! kill -0 "$server_pid" 2>/dev/null; then + test_fail "[RR10] RTSP server failed to start" "$output" + rm -f "$server_log" "$client_log" + return 1 + fi + + # --- Test: connect/disconnect cycling --- + local all_ok=true + local i + for i in $(seq 1 "$connect_cycles"); do + log_verbose "RTSP connect cycle $i/$connect_cycles ..." + + # Connect a client for a few seconds, then disconnect + timeout "$timeout_client" gst-launch-1.0 \ + rtspsrc location="$rtsp_url" latency=500 ! \ + rtph264depay ! h264parse ! avdec_h264 ! \ + fakesink sync=false num-buffers=30 \ + > "$client_log" 2>&1 + local cstat=$? + + # 0 = normal EOS exit, 124 = timeout (still counts as success + # since the stream was flowing) + if [ $cstat -ne 0 ] && [ $cstat -ne 124 ]; then + test_fail "[RR11] RTSP reconnection cycle $i/$connect_cycles (client exit $cstat)" "$output" + all_ok=false + break + fi + + # Verify server is still alive after client disconnect + if ! kill -0 "$server_pid" 2>/dev/null; then + test_fail "[RR12] RTSP server crashed after client disconnect (cycle $i)" + all_ok=false + break + fi + + log_verbose "RTSP cycle $i/$connect_cycles OK" + sleep "$CAMERA_RESET_DELAY" + done + [ "$all_ok" = true ] && test_pass "[RR11] RTSP server reconnection ($connect_cycles cycles)" + + # --- Test: two concurrent clients (shared factory) --- + if kill -0 "$server_pid" 2>/dev/null; then + local client1_log="/tmp/zed_rtsp_c1_$$.log" + local client2_log="/tmp/zed_rtsp_c2_$$.log" + + timeout "$timeout_client" gst-launch-1.0 \ + rtspsrc location="$rtsp_url" latency=500 ! \ + rtph264depay ! h264parse ! avdec_h264 ! \ + fakesink sync=false num-buffers=30 \ + > "$client1_log" 2>&1 & + local c1=$! + + sleep 2 + + timeout "$timeout_client" gst-launch-1.0 \ + rtspsrc location="$rtsp_url" latency=500 ! \ + rtph264depay ! h264parse ! avdec_h264 ! \ + fakesink sync=false num-buffers=30 \ + > "$client2_log" 2>&1 & + local c2=$! + + wait $c1 2>/dev/null + local c1s=$? + wait $c2 2>/dev/null + local c2s=$? + + if { [ $c1s -eq 0 ] || [ $c1s -eq 124 ]; } && \ + { [ $c2s -eq 0 ] || [ $c2s -eq 124 ]; }; then + test_pass "[RR13] RTSP multi-client (2 concurrent)" + else + test_fail "[RR14] RTSP multi-client (c1=$c1s, c2=$c2s)" + fi + + rm -f "$client1_log" "$client2_log" + else + skip_test "[RR15] RTSP multi-client" "Server no longer running" + fi + + # Cleanup + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + rm -f "$server_log" "$client_log" + + sleep $CAMERA_RESET_DELAY +} + +test_long_running_pipeline() { + print_subheader "Long-Running Pipeline Tests (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[LR01] Long-running zedsrc" "No camera detected" + skip_test "[LR02] Long-running demux+mux" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[LR03] Long-running pipeline tests" "Extensive mode required" + return 0 + fi + + local timeout_val=120 + local num_buffers=300 # ~10 seconds at 30fps + + # Sustained capture — exercises buffer pool recycling and timestamp + # monotonicity over many frames + local output + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=2 \ + num-buffers=$num_buffers ! queue max-size-buffers=10 ! fakesink sync=true 2>&1) + if [ $? -eq 0 ]; then + test_pass "[LR04] Long-running zedsrc ($num_buffers buffers)" + else + test_fail "[LR05] Long-running zedsrc ($num_buffers buffers)" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # Sustained demux→mux round-trip stresses chain functions, buffer + # ownership, and timestamp synchronisation over many frames. + output=$(timeout "$timeout_val" gst-launch-1.0 \ + zedsrc stream-type=2 num-buffers=$num_buffers ! \ + zeddemux stream-data=true name=demux \ + demux.src_left ! queue max-size-buffers=10 ! zeddatamux name=mux ! queue ! fakesink sync=true \ + demux.src_data ! queue max-size-buffers=10 ! mux.sink_data \ + demux.src_aux ! queue max-size-buffers=10 ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[LR06] Long-running demux+mux ($num_buffers buffers)" + else + test_fail "[LR07] Long-running demux+mux ($num_buffers buffers)" "$output" + fi + + sleep $CAMERA_RESET_DELAY +} + +test_full_pipeline_encode() { + print_subheader "Full Encode Pipeline Tests (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[FE01] H.264 encode pipeline" "No camera detected" + skip_test "[FE02] File sink record + verify" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[FE03] Full encode pipeline tests" "Extensive mode required" + return 0 + fi + + local timeout_val=60 + local num_buffers=60 # ~2 seconds at 30fps + local output + + build_h264_encode_pipeline encode + if [ "$_ENCODE_AVAILABLE" != true ]; then + skip_test "[FE04] H.264 encode pipeline" "No H.264 encoder available" + skip_test "[FE05] File sink record + verify" "No H.264 encoder available" + return 0 + fi + + local st="$_ENCODE_STREAM_TYPE" + local encoder="$_ENCODE_PIPELINE" + local encoder_name="$_ENCODER_NAME" + local cam_prop="$_ENCODE_CAMERA_PROP" + + # Test: camera → encode → fakesink (exercises real encode path) + # shellcheck disable=SC2086 + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=$st $cam_prop num-buffers=$num_buffers ! queue ! $encoder ! fakesink 2>&1) + if [ $? -eq 0 ]; then + test_pass "[FE06] H.264 encode pipeline ($encoder_name)" + else + test_fail "[FE07] H.264 encode pipeline ($encoder_name)" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # Test: camera → encode → file → verify file exists and is non-empty + local tmpfile="/tmp/zed_test_record_$$.mp4" + # shellcheck disable=SC2086 + output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=$st $cam_prop num-buffers=$num_buffers ! queue ! $encoder ! h264parse ! mp4mux ! filesink location=$tmpfile 2>&1) + if [ $? -eq 0 ] && [ -f "$tmpfile" ] && [ -s "$tmpfile" ]; then + local fsize + fsize=$(stat -c%s "$tmpfile" 2>/dev/null || echo 0) + test_pass "[FE08] File sink record + verify (${fsize} bytes, $encoder_name)" + else + test_fail "[FE09] File sink record + verify ($encoder_name)" "$output" + fi + rm -f "$tmpfile" + + sleep $CAMERA_RESET_DELAY +} + +# ============================================================================= +# Content Validation Tests (extensive mode) +# ============================================================================= +# These tests go beyond "did the pipeline crash?" — they verify that the +# output actually contains the expected video content (correct resolution, +# valid pixels, correct frame count, etc.). + +# Helper: use python3 to inspect a raw image file for non-black content. +# Reads raw BGRA file and checks that at least some pixels are non-zero. +# Usage: verify_frame_not_black +verify_frame_not_black() { + local file="$1" + local expected_bytes="$2" + python3 -c " +import sys, os +path = sys.argv[1] +expected = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2] != '0' else 0 +size = os.path.getsize(path) +if expected > 0 and size < expected: + print(f'WRONG_SIZE:{size}') + sys.exit(1) +data = open(path, 'rb').read() +nonzero = sum(1 for b in data[:min(len(data), 100000)] if b != 0) +ratio = nonzero / min(len(data), 100000) +if ratio < 0.01: + print(f'BLACK:{ratio:.4f}') + sys.exit(1) +print(f'OK:{ratio:.4f}') +sys.exit(0) +" "$file" "$expected_bytes" 2>/dev/null +} + +test_caps_validation() { + print_subheader "Negotiated Caps Validation (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[CV01] Caps validation: resolution match" "No camera detected" + skip_test "[CV02] Caps validation: format match" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[CV03] Caps validation" "Extensive mode required" + return 0 + fi + + local timeout_val=$EXTENSIVE_PIPELINE_TIMEOUT + local num_buffers=5 + + # Use the camera's default resolution and verify the negotiated caps + # contain valid width/height values. Previous versions hardcoded HD720 + # (camera-resolution=3) which is USB-only and fails on GMSL cameras. + local output + output=$(timeout "$timeout_val" gst-launch-1.0 -v \ + zedsrc stream-type=0 num-buffers=$num_buffers ! \ + fakesink 2>&1) + + if [ $? -eq 0 ]; then + # gst-launch -v prints the negotiated caps on the link line + local actual_w actual_h + actual_w=$(echo "$output" | grep -oP 'width=\(int\)\K[0-9]+' | head -1) + actual_h=$(echo "$output" | grep -oP 'height=\(int\)\K[0-9]+' | head -1) + if [ -n "$actual_w" ] && [ -n "$actual_h" ] && \ + [ "$actual_w" -ge 320 ] && [ "$actual_h" -ge 240 ]; then + test_pass "[CV04] Caps validation: resolution negotiated (${actual_w}x${actual_h})" + else + test_fail "[CV05] Caps validation: could not parse negotiated caps" "$output" + fi + else + test_fail "[CV06] Caps validation: pipeline failed to run" + fi + + sleep $CAMERA_RESET_DELAY + + # Verify BGRA format is negotiated for stream-type=0 + output=$(timeout "$timeout_val" gst-launch-1.0 -v \ + zedsrc stream-type=0 num-buffers=$num_buffers ! fakesink 2>&1) + + if [ $? -eq 0 ]; then + if echo "$output" | grep -q "format.*BGRA"; then + test_pass "[CV07] Caps validation: BGRA format negotiated" + else + local actual_fmt + actual_fmt=$(echo "$output" | grep -oP 'format=\(string\)\K\w+' | head -1) + test_fail "[CV08] Caps validation: expected BGRA, got ${actual_fmt:-unknown}" + fi + else + test_fail "[CV09] Caps validation: format check pipeline failed" + fi + + sleep $CAMERA_RESET_DELAY +} + +test_frame_content() { + print_subheader "Frame Content Validation (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[FC01] Frame content: non-black" "No camera detected" + skip_test "[FC02] Frame content: buffer count" "No camera detected" + skip_test "[FC03] Frame content: PNG snapshot" "No camera detected" + return 2 + fi + + if [ "$EXTENSIVE_MODE" = false ]; then + skip_test "[FC04] Frame content validation" "Extensive mode required" + return 0 + fi + + local timeout_val=$EXTENSIVE_PIPELINE_TIMEOUT + local tmpdir="/tmp/zed_frame_test_$$" + mkdir -p "$tmpdir" + + # --- Test 1: Capture raw frames and verify they are not all-black --- + # Use multifilesink to save exactly 1 raw BGRA frame + local output + output=$(timeout "$timeout_val" gst-launch-1.0 -v \ + zedsrc stream-type=0 num-buffers=1 ! \ + multifilesink location="${tmpdir}/frame_%05d.raw" max-files=1 2>&1) + + if [ $? -eq 0 ] && [ -f "${tmpdir}/frame_00000.raw" ]; then + local fsize + fsize=$(stat -c%s "${tmpdir}/frame_00000.raw" 2>/dev/null || echo 0) + + if [ "$fsize" -gt 1000 ]; then + local result + result=$(verify_frame_not_black "${tmpdir}/frame_00000.raw" 0) + if [ $? -eq 0 ]; then + test_pass "[FC05] Frame content: non-black ($fsize bytes, $result)" + else + test_fail "[FC06] Frame content: captured frame is black/empty ($result)" + fi + else + test_fail "[FC07] Frame content: raw frame too small ($fsize bytes)" + fi + else + test_fail "[FC08] Frame content: failed to capture raw frame" "$output" + fi + + sleep $CAMERA_RESET_DELAY + + # --- Test 2: Verify buffer count --- + # Request exactly N buffers and verify we get that many by writing + # each buffer to a separate file via multifilesink. + local expected_count=10 + local countdir="${tmpdir}/bufcount" + mkdir -p "$countdir" + output=$(timeout "$timeout_val" gst-launch-1.0 \ + zedsrc stream-type=0 num-buffers=$expected_count ! \ + multifilesink location="${countdir}/buf_%05d.raw" 2>&1) + + if [ $? -eq 0 ]; then + local actual_count + actual_count=$(find "$countdir" -name 'buf_*.raw' -type f 2>/dev/null | wc -l) + actual_count=${actual_count// /} # trim whitespace + + if [ "$actual_count" -ge "$expected_count" ]; then + test_pass "[FC09] Frame content: buffer count ($actual_count/$expected_count)" + else + test_fail "[FC10] Frame content: buffer count mismatch ($actual_count/$expected_count)" + fi else - test_fail "Camera auto exposure with ROI" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[FC11] Frame content: buffer count pipeline failed" fi - + sleep $CAMERA_RESET_DELAY - - # Test white balance control - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc \ - ctrl-whitebalance-auto=false ctrl-whitebalance-temperature=4500 \ - num-buffers=$num_buffers ! fakesink 2>&1) - if [ $? -eq 0 ]; then - test_pass "Camera white balance control" + + # --- Test 3: Save a PNG snapshot and verify it's a valid image --- + if gst-inspect-1.0 pngenc &>/dev/null; then + output=$(timeout "$timeout_val" gst-launch-1.0 \ + zedsrc stream-type=0 num-buffers=1 ! \ + videoconvert ! pngenc ! \ + filesink location="${tmpdir}/snapshot.png" 2>&1) + + if [ $? -eq 0 ] && [ -f "${tmpdir}/snapshot.png" ]; then + local png_size + png_size=$(stat -c%s "${tmpdir}/snapshot.png" 2>/dev/null || echo 0) + + # A valid PNG has header bytes 89 50 4E 47 (‰PNG) + local png_header + png_header=$(xxd -l4 -p "${tmpdir}/snapshot.png" 2>/dev/null) + + if [ "$png_header" = "89504e47" ] && [ "$png_size" -gt 1000 ]; then + test_pass "[FC12] Frame content: PNG snapshot valid ($png_size bytes)" + else + test_fail "[FC13] Frame content: PNG snapshot invalid (header=$png_header, size=$png_size)" + fi + else + test_fail "[FC14] Frame content: PNG snapshot pipeline failed" "$output" + fi else - test_fail "Camera white balance control" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + skip_test "[FC15] Frame content: PNG snapshot" "pngenc not available" fi - + + # Cleanup + rm -rf "$tmpdir" sleep $CAMERA_RESET_DELAY } -test_video_recording_playback() { - print_subheader "Video Recording/Playback Tests" - +test_demux_content() { + print_subheader "Demux Content Validation (requires camera)" + if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "Video recording" "No camera detected" - skip_test "Video playback" "No camera detected" + skip_test "[DC01] Demux content: left/right frames" "No camera detected" + skip_test "[DC02] Demux content: frame dimensions" "No camera detected" return 2 fi - + if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Video recording/playback tests" "Extensive mode required" + skip_test "[DC03] Demux content validation" "Extensive mode required" return 0 fi - + local timeout_val=90 - local test_dir="/tmp/zed_gst_test_$$" - local video_file="$test_dir/test_recording.mp4" + local tmpdir="/tmp/zed_demux_content_$$" + mkdir -p "$tmpdir" + + # Capture one frame from each demux output pad into separate raw files local output - - # Create test directory - mkdir -p "$test_dir" - - # STEP 1: Record video from camera to file - # Use mp4mux for a standard container format - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 num-buffers=60 ! \ - queue ! videoconvert ! video/x-raw,format=I420 ! \ - x264enc tune=zerolatency speed-preset=ultrafast ! \ - mp4mux ! filesink location="$video_file" 2>&1) - local record_status=$? - - if [ $record_status -eq 0 ] && [ -f "$video_file" ]; then - local file_size - file_size=$(stat -c%s "$video_file" 2>/dev/null || echo "0") - if [ "$file_size" -gt 1000 ]; then - test_pass "Video recording to MP4 ($file_size bytes)" - else - test_fail "Video recording (file too small: $file_size bytes)" - rm -rf "$test_dir" - return 1 - fi - else - # Try with openh264enc as fallback - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 num-buffers=60 ! \ - queue ! videoconvert ! video/x-raw,format=I420 ! \ - openh264enc ! \ - mp4mux ! filesink location="$video_file" 2>&1) - record_status=$? - - if [ $record_status -eq 0 ] && [ -f "$video_file" ]; then - local file_size - file_size=$(stat -c%s "$video_file" 2>/dev/null || echo "0") - if [ "$file_size" -gt 1000 ]; then - test_pass "Video recording to MP4 ($file_size bytes, openh264)" + output=$(timeout "$timeout_val" gst-launch-1.0 -v \ + zedsrc stream-type=2 num-buffers=3 ! \ + zeddemux is-depth=false name=demux \ + demux.src_left ! queue ! multifilesink location="${tmpdir}/left_%05d.raw" max-files=1 \ + demux.src_aux ! queue ! multifilesink location="${tmpdir}/right_%05d.raw" max-files=1 \ + 2>&1) + + if [ $? -eq 0 ]; then + # multifilesink with max-files=1 keeps only the last buffer, so the + # file index depends on num-buffers. Find whichever file was written. + local left_file right_file + left_file=$(ls -1 "${tmpdir}"/left_*.raw 2>/dev/null | head -1) + right_file=$(ls -1 "${tmpdir}"/right_*.raw 2>/dev/null | head -1) + + if [ -n "$left_file" ] && [ -f "$left_file" ] && \ + [ -n "$right_file" ] && [ -f "$right_file" ]; then + local left_size right_size + left_size=$(stat -c%s "$left_file" 2>/dev/null || echo 0) + right_size=$(stat -c%s "$right_file" 2>/dev/null || echo 0) + + # Both frames should be the same size (same resolution, same format) + if [ "$left_size" -gt 1000 ] && [ "$right_size" -gt 1000 ]; then + if [ "$left_size" -eq "$right_size" ]; then + test_pass "[DC04] Demux content: left/right frames same size ($left_size bytes each)" + else + test_fail "[DC05] Demux content: left ($left_size) != right ($right_size) size" + fi else - test_fail "Video recording (file too small)" - rm -rf "$test_dir" - return 1 + test_fail "[DC06] Demux content: frames too small (left=$left_size, right=$right_size)" + fi + + # Verify left frame is not black + if [ -n "$left_file" ] && [ -f "$left_file" ]; then + local result + result=$(verify_frame_not_black "$left_file" 0) + if [ $? -eq 0 ]; then + test_pass "[DC07] Demux content: left frame non-black" + else + test_fail "[DC08] Demux content: left frame is black ($result)" + fi fi else - test_fail "Video recording to MP4" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 - rm -rf "$test_dir" - return 1 + test_fail "[DC09] Demux content: missing output files" + fi + + # Verify negotiated dimensions from -v output + local left_w left_h + left_w=$(echo "$output" | grep "src_left" | grep -oP 'width=\(int\)\K[0-9]+' | head -1) + left_h=$(echo "$output" | grep "src_left" | grep -oP 'height=\(int\)\K[0-9]+' | head -1) + if [ -n "$left_w" ] && [ -n "$left_h" ] && [ "$left_w" -gt 0 ] && [ "$left_h" -gt 0 ]; then + test_pass "[DC10] Demux content: frame dimensions ${left_w}x${left_h}" + else + # Not fatal — some gst versions don't print pad caps in -v mode + log_verbose "Demux content: could not extract dimensions from pipeline output" fi - fi - - sleep 1 - - # STEP 2: Playback the recorded file - output=$(timeout 30 gst-launch-1.0 filesrc location="$video_file" ! \ - qtdemux ! h264parse ! avdec_h264 ! videoconvert ! fakesink 2>&1) - local playback_status=$? - - if [ $playback_status -eq 0 ]; then - test_pass "Video playback from recorded file" else - test_fail "Video playback from recorded file" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[DC11] Demux content: pipeline failed" "$output" fi - - # Cleanup - rm -rf "$test_dir" - + + rm -rf "$tmpdir" sleep $CAMERA_RESET_DELAY } -test_udp_streaming() { - print_subheader "UDP Streaming Tests (sender/receiver)" - +test_rtsp_content() { + print_subheader "RTSP Stream Content Validation (requires camera)" + if [ "$HARDWARE_AVAILABLE" = false ]; then - skip_test "UDP stream sender" "No camera detected" - skip_test "UDP stream receiver" "No camera detected" + skip_test "[RC01] RTSP content: subscriber frame capture" "No camera detected" + skip_test "[RC02] RTSP content: stream resolution" "No camera detected" return 2 fi - + if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "UDP streaming tests" "Extensive mode required" + skip_test "[RC03] RTSP content validation" "Extensive mode required" return 0 fi - - local timeout_val=30 - local stream_port=5000 - local num_buffers=90 # ~3 seconds at 30fps - local output - - # Find an available port - while netstat -tuln 2>/dev/null | grep -q ":$stream_port " || \ - ss -tuln 2>/dev/null | grep -q ":$stream_port "; do - stream_port=$((stream_port + 1)) - if [ $stream_port -gt 5100 ]; then - skip_test "UDP streaming" "No available port found" + + if ! command -v gst-zed-rtsp-launch &>/dev/null; then + skip_test "[RC04] RTSP content validation" "gst-zed-rtsp-launch not installed" + return 2 + fi + + if ! gst-inspect-1.0 rtspsrc &>/dev/null; then + skip_test "[RC05] RTSP content validation" "rtspsrc element not available" + return 2 + fi + + local rtsp_port=8654 + local rtsp_url="rtsp://127.0.0.1:${rtsp_port}/zed-stream" + local server_log="/tmp/zed_rtsp_content_server_$$.log" + local tmpdir="/tmp/zed_rtsp_content_$$" + local server_pid="" + local timeout_server_start=8 + local timeout_client=30 + + mkdir -p "$tmpdir" + + # Find a free port + while ss -tuln 2>/dev/null | grep -q ":$rtsp_port "; do + rtsp_port=$((rtsp_port + 1)) + rtsp_url="rtsp://127.0.0.1:${rtsp_port}/zed-stream" + if [ $rtsp_port -gt 8700 ]; then + skip_test "[RC06] RTSP content validation" "No available port" + rm -rf "$tmpdir" return 0 fi done - - # Start receiver in background first (must be ready before sender) - local receiver_output="/tmp/zed_udp_receiver_$$.log" - timeout "$timeout_val" gst-launch-1.0 \ - udpsrc port=$stream_port caps="application/x-rtp,media=video,encoding-name=H264" ! \ - rtph264depay ! h264parse ! avdec_h264 ! fakesink sync=false \ - > "$receiver_output" 2>&1 & - local receiver_pid=$! - - # Give receiver time to bind - sleep 2 - - # Check if receiver is still running - if ! kill -0 $receiver_pid 2>/dev/null; then - test_fail "UDP receiver failed to start" - rm -f "$receiver_output" + + # Build the RTSP server pipeline via the shared helper. + build_h264_encode_pipeline rtsp + if [ "$_ENCODE_AVAILABLE" != true ]; then + skip_test "[RC07] RTSP content validation" "No H.264 encoder available" + rm -rf "$tmpdir" + return 0 + fi + local rtsp_pipeline="zedsrc stream-type=$_ENCODE_STREAM_TYPE $_ENCODE_CAMERA_PROP ! queue ! $_ENCODE_PIPELINE" + + # Start RTSP server. + # NOTE: gst-zed-rtsp-launch uses gst_parse_launchv(), which expects + # the pipeline elements as separate argv words — leave unquoted. + # shellcheck disable=SC2086 + gst-zed-rtsp-launch -p "$rtsp_port" $rtsp_pipeline > "$server_log" 2>&1 & + server_pid=$! + sleep "$timeout_server_start" + + if ! kill -0 "$server_pid" 2>/dev/null; then + test_fail "[RC08] RTSP content: server failed to start" "$output" + rm -f "$server_log" + rm -rf "$tmpdir" return 1 fi - - # Start sender - stream camera to UDP - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 num-buffers=$num_buffers ! \ - queue ! videoconvert ! video/x-raw,format=I420 ! \ - x264enc tune=zerolatency speed-preset=ultrafast bitrate=2000 ! \ - rtph264pay ! udpsink host=127.0.0.1 port=$stream_port 2>&1) - local sender_status=$? - - # Give receiver a moment to process - sleep 1 - - # Stop receiver - kill $receiver_pid 2>/dev/null - wait $receiver_pid 2>/dev/null - - # Check results - if [ $sender_status -eq 0 ]; then - # Check if receiver got any data - if [ -f "$receiver_output" ]; then - local receiver_log - receiver_log=$(cat "$receiver_output") - if echo "$receiver_log" | grep -qi "PLAYING\|pipeline\|clock"; then - test_pass "UDP stream sender (to port $stream_port)" - test_pass "UDP stream receiver (verified data flow)" - else - test_pass "UDP stream sender (to port $stream_port)" - test_fail "UDP stream receiver (no data received)" - fi + + # --- Test 1: Subscribe and capture a PNG frame from the RTSP stream --- + local output + output=$(timeout "$timeout_client" gst-launch-1.0 -v \ + rtspsrc location="$rtsp_url" latency=500 ! \ + rtph264depay ! h264parse ! avdec_h264 ! \ + videoconvert ! video/x-raw,format=RGB ! \ + pngenc snapshot=true ! \ + filesink location="${tmpdir}/rtsp_frame.png" 2>&1) + local client_exit=$? + + # Both 0 (EOS) and 124 (timeout but wrote file) are acceptable + if { [ $client_exit -eq 0 ] || [ $client_exit -eq 124 ]; } && [ -f "${tmpdir}/rtsp_frame.png" ]; then + local png_size + png_size=$(stat -c%s "${tmpdir}/rtsp_frame.png" 2>/dev/null || echo 0) + local png_header + png_header=$(xxd -l4 -p "${tmpdir}/rtsp_frame.png" 2>/dev/null) + + if [ "$png_header" = "89504e47" ] && [ "$png_size" -gt 500 ]; then + test_pass "[RC09] RTSP content: subscriber frame capture (valid PNG, $png_size bytes)" else - test_pass "UDP stream sender (to port $stream_port)" - skip_test "UDP stream receiver" "Could not verify" + test_fail "[RC10] RTSP content: subscriber captured invalid image (header=$png_header, size=$png_size)" fi else - # Try with openh264enc as fallback - timeout "$timeout_val" gst-launch-1.0 \ - udpsrc port=$stream_port caps="application/x-rtp,media=video,encoding-name=H264" ! \ - rtph264depay ! h264parse ! avdec_h264 ! fakesink sync=false \ - > "$receiver_output" 2>&1 & - receiver_pid=$! - sleep 2 - - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc stream-type=0 num-buffers=$num_buffers ! \ - queue ! videoconvert ! video/x-raw,format=I420 ! \ - openh264enc ! \ - rtph264pay ! udpsink host=127.0.0.1 port=$stream_port 2>&1) - sender_status=$? - - kill $receiver_pid 2>/dev/null - wait $receiver_pid 2>/dev/null - - if [ $sender_status -eq 0 ]; then - test_pass "UDP stream sender (openh264, port $stream_port)" - test_pass "UDP stream receiver" + test_fail "[RC11] RTSP content: subscriber failed to capture frame (exit $client_exit)" "$output" + fi + + # --- Test 2: Verify stream resolution from negotiated caps --- + if echo "$output" | grep -qP 'width=\(int\)[0-9]+'; then + local stream_w stream_h + stream_w=$(echo "$output" | grep -oP 'width=\(int\)\K[0-9]+' | tail -1) + stream_h=$(echo "$output" | grep -oP 'height=\(int\)\K[0-9]+' | tail -1) + if [ -n "$stream_w" ] && [ -n "$stream_h" ] && \ + [ "$stream_w" -ge 320 ] && [ "$stream_h" -ge 240 ]; then + test_pass "[RC12] RTSP content: stream resolution ${stream_w}x${stream_h}" else - test_fail "UDP stream sender" - test_fail "UDP stream receiver" - [ "$VERBOSE" = true ] && echo "$output" | grep -i "error" | head -3 + test_fail "[RC13] RTSP content: unexpected resolution ${stream_w:-?}x${stream_h:-?}" fi + else + skip_test "[RC14] RTSP content: stream resolution" "Could not parse caps from output" fi - - rm -f "$receiver_output" + + # Cleanup + kill "$server_pid" 2>/dev/null + wait "$server_pid" 2>/dev/null + rm -f "$server_log" + rm -rf "$tmpdir" sleep $CAMERA_RESET_DELAY } -test_network_streaming() { - print_subheader "Network Streaming Tests" - - if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Network streaming tests" "Extensive mode required" - return 0 - fi - - local timeout_val=10 - local exit_code - local output - - # Test connecting to a non-existent stream (should fail or timeout) - # This tests error handling for invalid stream input - # Using a TEST-NET address (RFC 5737) that is guaranteed not to route - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc input-stream-ip="192.0.2.1" input-stream-port=30000 ! fakesink 2>&1) - exit_code=$? - - # Exit code 124 = timeout, non-zero = error - both are acceptable failures - if [ $exit_code -ne 0 ]; then - test_pass "Network stream invalid IP (failed as expected, exit code: $exit_code)" - else - # Unexpectedly succeeded - this shouldn't happen with an invalid IP - test_fail "Network stream invalid IP (should have failed)" +test_datamux_content() { + print_subheader "Data-Mux Content Validation (requires camera)" + + if [ "$HARDWARE_AVAILABLE" = false ]; then + skip_test "[MC01] Data-mux content: round-trip frame integrity" "No camera detected" + return 2 fi -} -test_error_handling() { - print_subheader "Error Handling Tests" - if [ "$EXTENSIVE_MODE" = false ]; then - skip_test "Error handling tests" "Extensive mode required" + skip_test "[MC02] Data-mux content validation" "Extensive mode required" return 0 fi - - local timeout_val=15 + + local timeout_val=90 + local num_buffers=5 + local tmpdir="/tmp/zed_mux_content_$$" + mkdir -p "$tmpdir" + + # Capture one frame BEFORE the mux (directly from demux left pad) + # and one frame AFTER the mux (round-tripped through datamux). + # Both should be the same size — the mux should not corrupt data. local output - - # Test invalid camera ID (should fail gracefully) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-id=99 ! fakesink 2>&1) - if [ $? -ne 0 ]; then - if echo "$output" | grep -qi "error\|failed\|not found\|open"; then - test_pass "Invalid camera ID (graceful failure)" - else - test_fail "Invalid camera ID (unexpected failure mode)" - fi - else - test_fail "Invalid camera ID (should have failed)" - fi - - # Test invalid serial number (should fail gracefully) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc camera-sn=999999999 ! fakesink 2>&1) - if [ $? -ne 0 ]; then - if echo "$output" | grep -qi "error\|failed\|not found\|open"; then - test_pass "Invalid serial number (graceful failure)" + output=$(timeout "$timeout_val" gst-launch-1.0 \ + zedsrc stream-type=2 num-buffers=$num_buffers ! \ + zeddemux stream-data=true name=demux \ + demux.src_left ! queue ! tee name=t \ + t. ! queue ! multifilesink location="${tmpdir}/pre_mux_%05d.raw" max-files=1 \ + t. ! queue ! zeddatamux name=mux ! queue ! multifilesink location="${tmpdir}/post_mux_%05d.raw" max-files=1 \ + demux.src_data ! queue ! mux.sink_data \ + demux.src_aux ! queue ! fakesink 2>&1) + + if [ $? -eq 0 ]; then + # multifilesink with max-files=1 keeps only the last buffer, so the + # file index depends on num-buffers. Find whichever file was written. + local pre post + pre=$(ls -1 "${tmpdir}"/pre_mux_*.raw 2>/dev/null | head -1) + post=$(ls -1 "${tmpdir}"/post_mux_*.raw 2>/dev/null | head -1) + + if [ -n "$pre" ] && [ -f "$pre" ] && \ + [ -n "$post" ] && [ -f "$post" ]; then + local pre_size post_size + pre_size=$(stat -c%s "$pre" 2>/dev/null || echo 0) + post_size=$(stat -c%s "$post" 2>/dev/null || echo 0) + + if [ "$pre_size" -gt 1000 ] && [ "$post_size" -gt 1000 ]; then + # The muxed output includes appended metadata, so post >= pre + if [ "$post_size" -ge "$pre_size" ]; then + test_pass "[MC03] Data-mux content: round-trip frame integrity (pre=$pre_size, post=$post_size)" + else + test_fail "[MC04] Data-mux content: post-mux smaller than pre-mux (pre=$pre_size, post=$post_size)" + fi + else + test_fail "[MC05] Data-mux content: frames too small (pre=$pre_size, post=$post_size)" + fi else - test_fail "Invalid serial number (unexpected failure mode)" + test_fail "[MC06] Data-mux content: missing output files" fi else - test_fail "Invalid serial number (should have failed)" - fi - - # Test invalid SVO file path (should fail gracefully) - output=$(timeout "$timeout_val" gst-launch-1.0 zedsrc svo-file="/nonexistent/path/video.svo" ! fakesink 2>&1) - if [ $? -ne 0 ]; then - # Any failure is acceptable - the key is it doesn't hang or crash - test_pass "Invalid SVO file path (graceful failure)" - else - test_fail "Invalid SVO file path (should have failed)" - fi - - # Test CSV sink with invalid location (should fail gracefully) - output=$(timeout "$timeout_val" gst-launch-1.0 videotestsrc num-buffers=1 ! \ - "application/data" ! zeddatacsvsink location="/nonexistent/dir/test.csv" 2>&1) - if [ $? -ne 0 ]; then - # Any failure is acceptable - test_pass "CSV sink invalid path (graceful failure)" - else - test_fail "CSV sink invalid path (should have failed)" - fi - - # Test zedodoverlay with non-video input (should fail or handle gracefully) - output=$(timeout "$timeout_val" gst-launch-1.0 audiotestsrc num-buffers=1 ! \ - zedodoverlay ! fakesink 2>&1) - if [ $? -ne 0 ]; then - # Any failure is acceptable - caps negotiation failure is expected - test_pass "Overlay with invalid input (graceful failure)" - else - test_fail "Overlay with invalid input (should have failed)" + test_fail "[MC07] Data-mux content: round-trip pipeline failed" "$output" fi + + rm -rf "$tmpdir" + sleep $CAMERA_RESET_DELAY } # ============================================================================= @@ -2878,14 +4264,25 @@ main() { # Check for hardware check_hardware + # ZED_Explorer probes all cameras through the Argus daemon. + # On multi-camera rigs (e.g. 6 GMSL cameras) the daemon needs a + # few seconds to release its resources before a new open succeeds. + if [ "$HARDWARE_AVAILABLE" = true ] && [ "$GMSL_CAMERA" = true ]; then + log_verbose "Waiting ${CAMERA_RESET_DELAY}s for Argus daemon to release camera resources..." + sleep "$CAMERA_RESET_DELAY" + fi + # Run plugin tests (always run these) test_plugin_registration test_plugin_properties test_plugin_factory test_zedsrc_enums + test_zedsrc_auto_resolution test_zedsrc_nv12 test_element_pads test_zedxone + test_zedxone_enums + test_zedxone_auto_resolution test_zedxone_nv12 # Hardware tests (if available) @@ -2894,6 +4291,13 @@ main() { test_hardware_streaming test_hardware_demux test_csv_sink + + # Lifecycle & reconnection regression tests (always run with hardware) + test_source_stop_start + test_zedxone_stop_start + test_demux_lifecycle + test_datamux_lifecycle + test_rtsp_reconnection if [ "$EXTENSIVE_MODE" = true ]; then test_latency_query @@ -2906,11 +4310,19 @@ main() { test_video_recording_playback test_udp_streaming test_zedxone_hardware + test_zedxone_resolutions test_hardware_od test_hardware_bt test_overlay_skeletons test_network_streaming test_error_handling + test_long_running_pipeline + test_full_pipeline_encode + test_caps_validation + test_frame_content + test_demux_content + test_rtsp_content + test_datamux_content fi else # Show skipped hardware tests @@ -2918,6 +4330,14 @@ main() { test_hardware_streaming test_hardware_demux test_csv_sink + + # Lifecycle & reconnection (will skip with reason) + test_source_stop_start + test_zedxone_stop_start + test_demux_lifecycle + test_datamux_lifecycle + test_rtsp_reconnection + if [ "$EXTENSIVE_MODE" = true ]; then test_latency_query test_buffer_metadata @@ -2929,11 +4349,19 @@ main() { test_video_recording_playback test_udp_streaming test_zedxone_hardware + test_zedxone_resolutions test_hardware_od test_hardware_bt test_overlay_skeletons test_network_streaming test_error_handling + test_long_running_pipeline + test_full_pipeline_encode + test_caps_validation + test_frame_content + test_demux_content + test_rtsp_content + test_datamux_content fi fi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4dd1a50..7514350 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,34 @@ LATEST CHANGES ============== +ZED SDK v5.2 +-------------- +- Add NV12 zero-copy capture support on Jetson (GMSL cameras) via ``stream-type=6`` +- Add NV12 stereo side-by-side zero-copy mode via ``stream-type=7`` +- Add AUTO stream-type negotiation (``stream-type=-1``) that prefers NV12 zero-copy when available +- Add AUTO resolution mode that selects the best resolution for requested FPS based on camera model +- Add flexible output resolution negotiation with ranged caps for non-NVMM formats +- Add BGR and GRAY8 output format support for zedsrc and zedxonesrc +- Add ``fixate`` callback for proper caps negotiation during pipeline setup +- Add side-by-side left+right image stream type (``stream-type=5``, #79) +- Add Jetson pipeline scripts: NV12 encoding, RTSP/SRT/UDP streaming, benchmarks +- Add runtime SVO recording control (``svo-recording-enable``, ``svo-recording-filename``, ``svo-recording-compression``, #78) +- Add ``output-rectified-image`` property for ZED X One (#71) +- Add GEN_3 positional tracking, NEURAL_LIGHT depth mode, custom YOLO model support +- Add SDK version compile-time check to catch version mismatches early +- Add test script (``.ci/test.sh``) with fast, extensive, and benchmark modes +- Default camera FPS changed from 15 to 30 +- Default camera resolution changed to AUTO +- Deprecated ``async-image-retrieval`` property in zedsrc (no-op, kept for backward compatibility) +- Fixed incorrect VGA resolution mapping and ``memcpy`` buffer overflow in data mux +- Fixed ``sprintf`` buffer safety issues +- Fixed multi-instance issues (replaced static buffer counters with per-instance variables) +- Fixed floating-point math issues and AEC_AGC enum handling +- Added latency query handlers to prevent crashes during pipeline queries +- Disabled shadow warnings for older ZED SDK compatibility +- Improved code formatting consistency and documentation typos +- SDK version requirement updated to 5.2 + ZED SDK v5.1 -------------- - Fix memleak on demux chain diff --git a/README.md b/README.md index c9f74f1..cc75923 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ GStreamer package for ZED Cameras. The package is composed of several elements: * [`zedsrc`](./gst-zed-src): acquires camera color image and depth map and pushes them in a GStreamer pipeline. Supports zero-copy NV12 output on Jetson for GMSL cameras (ZED X, ZED X Mini) with SDK 5.2+. -* [`zedxonesrc`](./gst-zedxone-src): acquires camera color image from a ZED X One GS or ZED X One 4K camera and pushes them in a GStreamer pipeline. Note: this element does not use the ZED SDK, but a porting of the [zedx-one-capture](https://github.com/stereolabs/zedx-one-capture) library. +* [`zedxonesrc`](./gst-zedxone-src): acquires camera color image from a ZED X One GS or ZED X One 4K camera and pushes them in a GStreamer pipeline. Uses the ZED SDK (`sl::CameraOne` API). Supports zero-copy NV12 output on Jetson with SDK 5.2+. * [`zedmeta`](./gst-zed-meta): GStreamer library to define and handle the ZED metadata (Positional Tracking data, Sensors data, Detected Object data, Detected Skeletons data). * [`zeddemux`](./gst-zed-demux): receives a composite `zedsrc` stream (`color left + color right` data or `color left + depth map` + metadata), processes the eventual depth data and pushes them in two separated new streams named `src_left` and `src_aux`. A third source pad is created for metadata to be externally processed. @@ -34,13 +34,13 @@ GStreamer package for ZED Cameras. The package is composed of several elements: ### Prerequisites -* [ZED SDK v5.1](https://www.stereolabs.com/developers/release/5.1) +* [ZED SDK v5.2](https://www.stereolabs.com/developers/release/5.2) * CMake (v3.6+) * GStreamer 1.0 ### Windows installation -* Install the latest ZED SDK from the [official download page](https://www.stereolabs.com/developers/release/5.1) [Optional to compile the `zedsrc` element to acquire data from a ZED camera device] +* Install the latest ZED SDK from the [official download page](https://www.stereolabs.com/developers/release/5.2) [Optional to compile the `zedsrc` element to acquire data from a ZED camera device] * Install [Git](https://git-scm.com/) or download a ZIP archive * Install [CMake](https://cmake.org/) * Install a [GStreamer distribution (**both `runtime` and `development` installers**)](https://gstreamer.freedesktop.org/download/). @@ -61,7 +61,7 @@ GStreamer package for ZED Cameras. The package is composed of several elements: #### Install prerequisites -* Install the latest ZED SDK from the [official download page](https://www.stereolabs.com/developers/release/5.1) +* Install the latest ZED SDK from the [official download page](https://www.stereolabs.com/developers/release/5.2) * Update the list of `apt` available packages @@ -136,6 +136,9 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r area-file-path : Area localization file that describes the surroundings, saved from a previous tracking session. flags: readable, writable String. Default: "" + async-grab-camera-recovery: Async Grab Camera Recovery + flags: readable, writable + Boolean. Default: false blocksize : Size in bytes to read per buffer (-1 = default) flags: readable, writable Unsigned Integer. Range: 0 - 4294967295 Default: 4096 @@ -183,7 +186,7 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r Boolean. Default: false camera-fps : Camera frame rate flags: readable, writable - Enum "GstZedSrcFPS" Default: 15, "15 FPS" + Enum "GstZedSrcFPS" Default: 30, "30 FPS" (120): 120 FPS - only SVGA (GMSL2) resolution (100): 100 FPS - only VGA (USB3) resolution (60): 60 FPS - VGA (USB3), HD720, HD1080 (GMSL2), and HD1200 (GMSL2) resolutions @@ -216,7 +219,7 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r Integer. Range: 0 - 100 Default: 50 coordinate-system : 3D Coordinate System flags: readable, writable - Enum "GstZedsrcStreamType" Default: 0, "Image" + Enum "GstZedsrcCoordSys" Default: 0, "Image" (0): Image - Standard coordinates system in computer vision. Used in OpenCV. (1): Left handed, Y up - Left-Handed with Y up and Z forward. Used in Unity with DirectX. (2): Right handed, Y up - Right-Handed with Y pointing up and Z backward. Used in OpenGL. @@ -308,6 +311,12 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r enable-area-memory : This mode enables the camera to remember its surroundings. This helps correct positional tracking drift, and can be helpful for positioning different cameras relative to one other in space. flags: readable, writable Boolean. Default: true + enable-image-enhancement: Enable Image Enhancement + flags: readable, writable + Boolean. Default: true + enable-image-validity-check: Enable Image Validity Check + flags: readable, writable + Boolean. Default: true enable-imu-fusion : This setting allows you to enable or disable IMU fusion. When set to false, only the optical odometry will be used. flags: readable, writable Boolean. Default: true @@ -320,6 +329,9 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r fill-mode : Specify the Depth Fill Mode flags: readable, writable Boolean. Default: false + grab-compute-capping-fps: Grab Compute Capping FPS + flags: readable, writable + Float. Range: 0 - 1000 Default: 0 initial-world-transform-pitch: Pitch orientation of the camera in the world frame when the camera is started flags: readable, writable Float. Range: 0 - 360 Default: 0 @@ -344,6 +356,12 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r input-stream-port : Specify port when using streaming input flags: readable, writable Integer. Range: 1 - 65535 Default: 30000 + max-working-res-h : Maximum Working Resolution Height + flags: readable, writable + Integer. Range: 0 - 10000 Default: 0 + max-working-res-w : Maximum Working Resolution Width + flags: readable, writable + Integer. Range: 0 - 10000 Default: 0 measure3D-reference-frame: Specify the 3D Reference Frame flags: readable, writable Enum "GstZedsrc3dMeasRefFrame" Default: 0, "WORLD" @@ -412,15 +430,27 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r od-enabled : Set to TRUE to enable Object Detection flags: readable, writable Boolean. Default: false + od-instance-id : Object Detection Instance ID + flags: readable, writable + Unsigned Integer. Range: 0 - 4294967295 Default: 0 + od-max-range : Maximum Detection Range + flags: readable, writable + Boolean. Default: false od-max-range : Maximum Detection Range flags: readable, writable Float. Range: -1 - 20000 Default: 20000 od-prediction-timeout-s: Object prediction timeout (sec) flags: readable, writable Float. Range: 0 - 1 Default: 0.2 + open-timeout-sec : Open Timeout Seconds + flags: readable, writable + Float. Range: 0 - 600 Default: 5 opencv-calibration-file: Optional OpenCV Calibration File flags: readable, writable String. Default: "" + optional-settings-path: Optional Settings Path + flags: readable, writable + String. Default: "" parent : The parent of the object flags: readable, writable, 0x2000 Object of type "GstObject" @@ -445,12 +475,24 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r roi-x : Region of interest top left 'X' coordinate (-1 to not set ROI) flags: readable, writable Integer. Range: -1 - 2208 Default: -1 + remove-saturated-areas: Remove Saturated Areas + flags: readable, writable + Boolean. Default: false roi-y : Region of interest top left 'Y' coordinate (-1 to not set ROI) flags: readable, writable Integer. Range: -1 - 1242 Default: -1 + sdk-gpu-id : SDK GPU ID + flags: readable, writable + Integer. Range: -1 - 128 Default: -1 sdk-verbose : ZED SDK Verbose level flags: readable, writable Integer. Range: 0 - 1000 Default: 0 + sdk-verbose-log-file: SDK Verbose Log File + flags: readable, writable + String. Default: "" + sensors-required : Sensors Required + flags: readable, writable + Boolean. Default: false set-as-static : Set to TRUE if the camera is static flags: readable, writable Boolean. Default: false @@ -462,15 +504,22 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r Boolean. Default: true stream-type : Image stream type flags: readable, writable - Enum "GstZedSrcCoordSys" Default: 0, "Left image [BGRA]" - (0): Left image [BGRA] - 8 bits- 4 channels Left image - (1): Right image [BGRA] - 8 bits- 4 channels Right image - (2): Stereo couple up/down [BGRA] - 8 bits- 4 channels bit Left and Right + Enum "GstZedSrcStreamType" Default: 0, "Left image [BGRA/BGR/GRAY8]" + (-1): Auto [prefer NV12 zero-copy] - Auto-negotiate format based on downstream + (0): Left image [BGRA/BGR/GRAY8] - Left image + (1): Right image [BGRA/BGR/GRAY8] - Right image + (2): Stereo couple up/down [BGRA/BGR/GRAY8] - Left and Right top/bottom (3): Depth image [GRAY16_LE] - 16 bits depth - (4): Left and Depth up/down [BGRA] - 8 bits- 4 channels Left and Depth(image) + (4): Left and Depth up/down [BGRA/BGR/GRAY8] - Left and Depth(image) top/bottom + (5): Stereo couple left/right [BGRA/BGR/GRAY8] - Left and Right side-by-side + (6): Raw NV12 zero-copy [NV12] - Zero-copy NV12 raw buffer (GMSL cameras only) + (7): Raw NV12 stereo zero-copy [NV12] - Zero-copy NV12 stereo side-by-side svo-file-path : Input from SVO file flags: readable, writable String. Default: "" + svo-real-time-mode : SVO Real Time Mode + flags: readable, writable + Boolean. Default: false svo-recording-compression: Compression mode for SVO recording flags: readable, writable Enum "GstZedsrcSvoCompression" Default: 2, "H265" @@ -493,6 +542,9 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r ### `ZED X One Video Source Element` properties ```bash + async-grab-camera-recovery: Async Grab Camera Recovery + flags: readable, writable + Boolean. Default: false blocksize : Size in bytes to read per buffer (-1 = default) flags: readable, writable Unsigned Integer. Range: 0 - 4294967295 Default: 4096 @@ -523,6 +575,23 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r camera-timeout : Connection opening timeout in seconds flags: readable, writable Float. Range: 0.5 - 86400 Default: 5 + coordinate-system : SDK Coordinate System + flags: readable, writable + Enum "GstZedXOneSrcCoordSys" Default: 0, "IMAGE" + (0): IMAGE - Standard image (0,0) at top left corner + (1): LEFT_HANDED_Y_UP - Left handed, Y up and Z forward + (2): RIGHT_HANDED_Y_UP - Right handed, Y up and Z backward + (3): RIGHT_HANDED_Z_UP - Right handed, Z up and Y forward + (4): LEFT_HANDED_Z_UP - Left handed, Z up and Y forward + (5): RIGHT_HANDED_Z_UP_X_FWD - Right handed, Z up and X forward + coordinate-units : SDK Coordinate Units + flags: readable, writable + Enum "GstZedXOneSrcUnit" Default: 0, "MILLIMETER" + (0): MILLIMETER - Millimeter + (1): CENTIMETER - Centimeter + (2): METER - Meter + (3): INCH - Inch + (4): FOOT - Foot ctrl-analog-gain : Camera control: Analog Gain value flags: readable, writable Integer. Range: 1000 - 30000 Default: 30000 @@ -586,6 +655,12 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r enable-hdr : Enable HDR if supported by resolution and frame rate. flags: readable, writable Boolean. Default: false + input-stream-ip : Specify IP address when using streaming input + flags: readable, writable + String. Default: "" + input-stream-port : Specify port when using streaming input + flags: readable, writable + Integer. Range: 1 - 65535 Default: 30000 name : The name of the object flags: readable, writable, 0x2000 String. Default: "zedxonesrc0" @@ -595,12 +670,30 @@ Most of the properties follow the same name as the C++ API. Except that `_` is r opencv-calibration-file: Optional OpenCV Calibration File flags: readable, writable String. Default: "" + optional-settings-path: Optional Settings Path + flags: readable, writable + String. Default: "" output-rectified-image: Enable image rectification (disable for custom optics without calibration) flags: readable, writable Boolean. Default: true parent : The parent of the object flags: readable, writable, 0x2000 Object of type "GstObject" + sdk-verbose-log-file: SDK Verbose Log File + flags: readable, writable + String. Default: "" + stream-type : Image stream type + flags: readable, writable + Enum "GstZedXOneSrcStreamType" Default: -1, "Auto [prefer NV12 zero-copy]" + (-1): Auto [prefer NV12 zero-copy] - Auto-negotiate format based on downstream + (0): Image [BGRA/BGR/GRAY8] - Single image + (1): Raw NV12 zero-copy [NV12] - Zero-copy NV12 raw buffer (Jetson only) + svo-file-path : Input from SVO file + flags: readable, writable + String. Default: "" + svo-real-time-mode : SVO Real Time Mode + flags: readable, writable + Boolean. Default: false typefind : Run typefind before negotiating (deprecated, non-functional) flags: readable, writable, deprecated Boolean. Default: false diff --git a/gst-zed-data-mux/gstzeddatamux.cpp b/gst-zed-data-mux/gstzeddatamux.cpp index 63e1c4f..2eef093 100644 --- a/gst-zed-data-mux/gstzeddatamux.cpp +++ b/gst-zed-data-mux/gstzeddatamux.cpp @@ -274,6 +274,8 @@ static void gst_zeddatamux_set_property(GObject *object, guint prop_id, const GV GParamSpec *pspec); static void gst_zeddatamux_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); +static GstStateChangeReturn gst_zeddatamux_change_state(GstElement *element, + GstStateChange transition); static gboolean gst_zeddatamux_sink_data_event(GstPad *pad, GstObject *parent, GstEvent *event); static GstFlowReturn gst_zeddatamux_chain_data(GstPad *pad, GstObject *parent, GstBuffer *buf); @@ -283,6 +285,8 @@ static GstFlowReturn gst_zeddatamux_chain_video(GstPad *pad, GstObject *parent, /* GObject vmethod implementations */ /* initialize the plugin's class */ +static void gst_zeddatamux_finalize(GObject *object); + static void gst_zeddatamux_class_init(GstZedDataMuxClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); GstElementClass *gstelement_class = GST_ELEMENT_CLASS(klass); @@ -291,6 +295,9 @@ static void gst_zeddatamux_class_init(GstZedDataMuxClass *klass) { gobject_class->set_property = gst_zeddatamux_set_property; gobject_class->get_property = gst_zeddatamux_get_property; + gobject_class->finalize = gst_zeddatamux_finalize; + + gstelement_class->change_state = gst_zeddatamux_change_state; gst_element_class_set_static_metadata(gstelement_class, "ZED Data Video Muxer", "Muxer/Video", "Stereolabs ZED Data Video Muxer", @@ -341,6 +348,56 @@ static void gst_zeddatamux_init(GstZedDataMux *filter) { filter->last_data_buf_size = 0; } +static void gst_zeddatamux_finalize(GObject *object) { + GstZedDataMux *filter = GST_ZEDDATAMUX(object); + + if (filter->caps) { + gst_caps_unref(filter->caps); + filter->caps = nullptr; + } + if (filter->last_video_buf) { + gst_buffer_unref(filter->last_video_buf); + filter->last_video_buf = nullptr; + } + if (filter->last_data_buf) { + gst_buffer_unref(filter->last_data_buf); + filter->last_data_buf = nullptr; + } + + G_OBJECT_CLASS(gst_zeddatamux_parent_class)->finalize(object); +} + +static GstStateChangeReturn gst_zeddatamux_change_state(GstElement *element, + GstStateChange transition) { + GstZedDataMux *filter = GST_ZEDDATAMUX(element); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + case GST_STATE_CHANGE_READY_TO_NULL: + if (filter->caps) { + gst_caps_unref(filter->caps); + filter->caps = nullptr; + } + if (filter->last_video_buf) { + gst_buffer_unref(filter->last_video_buf); + filter->last_video_buf = nullptr; + } + if (filter->last_data_buf) { + gst_buffer_unref(filter->last_data_buf); + filter->last_data_buf = nullptr; + } + filter->last_video_ts = 0; + filter->last_data_ts = 0; + filter->last_video_buf_size = 0; + filter->last_data_buf_size = 0; + break; + default: + break; + } + + return GST_ELEMENT_CLASS(gst_zeddatamux_parent_class)->change_state(element, transition); +} + static void gst_zeddatamux_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GstZedDataMux *filter = GST_ZEDDATAMUX(object); @@ -493,55 +550,67 @@ static GstFlowReturn gst_zeddatamux_chain_data(GstPad *pad, GstObject *parent, G // ----> Release incoming buffer GST_TRACE("Input buffer unmap"); gst_buffer_unmap(buf, &map_in); - // gst_buffer_unref(buf); + gst_buffer_unref(buf); // <---- Release incoming buffer return GST_FLOW_ERROR; } - if (gst_buffer_map(out_buf, &map_out, (GstMapFlags) (GST_MAP_WRITE)) && - gst_buffer_map(filter->last_video_buf, &map_store, - (GstMapFlags) (GST_MAP_READ))) { - GST_TRACE("Copying video buffer %lu B", out_buf_size); - memcpy(map_out.data, map_store.data, out_buf_size); - - GstZedSrcMeta *meta = (GstZedSrcMeta *) buf; - - GST_TRACE("Adding metadata"); - gst_buffer_add_zed_src_meta(out_buf, meta->info, meta->pose, meta->sens, - meta->od_enabled, meta->obj_count, meta->objects, - meta->frame_id); - - // ----> Timestamp meta-data - GST_TRACE("Out buffer set timestamp"); - GST_BUFFER_TIMESTAMP(out_buf) = timestamp; - GST_BUFFER_DTS(out_buf) = GST_BUFFER_TIMESTAMP(out_buf); - GST_BUFFER_OFFSET(out_buf) = GST_BUFFER_OFFSET(filter->last_video_buf); - // <---- Timestamp meta-data - - GST_TRACE("Out buffer push"); - GstFlowReturn ret = gst_pad_push(filter->srcpad, out_buf); - - if (ret != GST_FLOW_OK) { - GST_DEBUG_OBJECT(filter, "Error pushing out buffer: %s", - gst_flow_get_name(ret)); - - // ----> Release incoming buffer - GST_TRACE("Input buffer unmap"); - gst_buffer_unmap(buf, &map_in); - // gst_buffer_unref(buf); - GST_TRACE("Out buffer unmap"); - gst_buffer_unmap(out_buf, &map_out); - // gst_buffer_unref(out_buf); - // <---- Release incoming buffer - return ret; - } - - GST_TRACE("Out buffer unmap"); + if (!gst_buffer_map(out_buf, &map_out, (GstMapFlags) (GST_MAP_WRITE))) { + GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, + ("Failed to map output buffer for writing"), (NULL)); + gst_buffer_unref(out_buf); + gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); + return GST_FLOW_ERROR; + } + if (!gst_buffer_map(filter->last_video_buf, &map_store, + (GstMapFlags) (GST_MAP_READ))) { + GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, + ("Failed to map stored video buffer for reading"), (NULL)); gst_buffer_unmap(out_buf, &map_out); - GST_TRACE("Store buffer unmap"); - gst_buffer_unmap(filter->last_video_buf, &map_store); - // gst_buffer_unref(data_buf); + gst_buffer_unref(out_buf); + gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); + return GST_FLOW_ERROR; + } + + GST_TRACE("Copying video buffer %lu B", out_buf_size); + memcpy(map_out.data, map_store.data, out_buf_size); + + // The data buffer's payload contains a serialized GstZedSrcMeta. + // Read from the mapped data, NOT from the GstBuffer pointer. + GstZedSrcMeta *meta = (GstZedSrcMeta *) map_in.data; + + GST_TRACE("Adding metadata"); + gst_buffer_add_zed_src_meta(out_buf, meta->info, meta->pose, meta->sens, + meta->od_enabled, meta->obj_count, meta->objects, + meta->frame_id); + + // ----> Timestamp meta-data + GST_TRACE("Out buffer set timestamp"); + GST_BUFFER_TIMESTAMP(out_buf) = timestamp; + GST_BUFFER_DTS(out_buf) = GST_BUFFER_TIMESTAMP(out_buf); + GST_BUFFER_OFFSET(out_buf) = GST_BUFFER_OFFSET(filter->last_video_buf); + // <---- Timestamp meta-data + + // Unmap all buffers before push (push takes ownership of out_buf) + GST_TRACE("Out buffer unmap"); + gst_buffer_unmap(out_buf, &map_out); + GST_TRACE("Store buffer unmap"); + gst_buffer_unmap(filter->last_video_buf, &map_store); + + GST_TRACE("Out buffer push"); + GstFlowReturn ret = gst_pad_push(filter->srcpad, out_buf); + // out_buf ownership transferred to downstream, do not touch it + + if (ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT(filter, "Error pushing out buffer: %s", + gst_flow_get_name(ret)); + + gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); + return ret; } } else { GST_TRACE("No video buffer to be muxed"); @@ -551,15 +620,15 @@ static GstFlowReturn gst_zeddatamux_chain_data(GstPad *pad, GstObject *parent, G filter->last_data_ts = timestamp; - if (!filter->last_data_buf) { - GST_TRACE("Creating new stored data buffer"); + if (!filter->last_data_buf || map_in.size != filter->last_data_buf_size) { + GST_TRACE("%s stored data buffer (size %lu)", + filter->last_data_buf ? "Reallocating" : "Creating", map_in.size); + if (filter->last_data_buf) { + gst_buffer_unref(filter->last_data_buf); + } filter->last_data_buf_size = map_in.size; filter->last_data_buf = gst_buffer_new_allocate(NULL, filter->last_data_buf_size, NULL); - } else if (map_in.size != filter->last_data_buf_size) { - GST_TRACE("Resizing stored data buffer"); - filter->last_data_buf_size = map_in.size; - gst_buffer_resize(filter->last_data_buf, 0, filter->last_data_buf_size); } if (!GST_IS_BUFFER(filter->last_data_buf)) { @@ -568,7 +637,7 @@ static GstFlowReturn gst_zeddatamux_chain_data(GstPad *pad, GstObject *parent, G // ----> Release incoming buffer GST_TRACE("Input buffer unmap"); gst_buffer_unmap(buf, &map_in); - // gst_buffer_unref(buf); + gst_buffer_unref(buf); // <---- Release incoming buffer return GST_FLOW_ERROR; @@ -586,9 +655,11 @@ static GstFlowReturn gst_zeddatamux_chain_data(GstPad *pad, GstObject *parent, G // ----> Release incoming buffer GST_TRACE("Input buffer unmap"); gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); // <---- Release incoming buffer } else { GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, ("Failed to map buffer for reading"), (NULL)); + gst_buffer_unref(buf); return GST_FLOW_ERROR; } GST_TRACE("... processed"); @@ -631,55 +702,65 @@ static GstFlowReturn gst_zeddatamux_chain_video(GstPad *pad, GstObject *parent, // ----> Release incoming buffer GST_TRACE("Input buffer unmap"); gst_buffer_unmap(buf, &map_in); - // gst_buffer_unref(buf); + gst_buffer_unref(buf); // <---- Release incoming buffer return GST_FLOW_ERROR; } - if (gst_buffer_map(out_buf, &map_out, (GstMapFlags) (GST_MAP_WRITE)) && - gst_buffer_map(filter->last_data_buf, &map_store, - (GstMapFlags) (GST_MAP_WRITE))) { - GST_TRACE("Copying video buffer %lu B", map_in.size); - memcpy(map_out.data, map_in.data, map_in.size); - - GstZedSrcMeta *meta = (GstZedSrcMeta *) map_store.data; - - GST_TRACE("Adding metadata"); - gst_buffer_add_zed_src_meta(out_buf, meta->info, meta->pose, meta->sens, - meta->od_enabled, meta->obj_count, meta->objects, - meta->frame_id); - - // ----> Timestamp meta-data - GST_TRACE("Out buffer set timestamp"); - GST_BUFFER_TIMESTAMP(out_buf) = timestamp; - GST_BUFFER_DTS(out_buf) = GST_BUFFER_TIMESTAMP(out_buf); - GST_BUFFER_OFFSET(out_buf) = GST_BUFFER_OFFSET(buf); - // <---- Timestamp meta-data - - GST_TRACE("Out buffer push"); - GstFlowReturn ret = gst_pad_push(filter->srcpad, out_buf); - - if (ret != GST_FLOW_OK) { - GST_DEBUG_OBJECT(filter, "Error pushing out buffer: %s", - gst_flow_get_name(ret)); - - // ----> Release incoming buffer - GST_TRACE("Input buffer unmap"); - gst_buffer_unmap(buf, &map_in); - // gst_buffer_unref(buf); - GST_TRACE("Out buffer unmap"); - gst_buffer_unmap(out_buf, &map_out); - // gst_buffer_unref(out_buf); - // <---- Release incoming buffer - return ret; - } - - GST_TRACE("Out buffer unmap"); + if (!gst_buffer_map(out_buf, &map_out, (GstMapFlags) (GST_MAP_WRITE))) { + GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, + ("Failed to map output buffer for writing"), (NULL)); + gst_buffer_unref(out_buf); + gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); + return GST_FLOW_ERROR; + } + if (!gst_buffer_map(filter->last_data_buf, &map_store, + (GstMapFlags) (GST_MAP_READ))) { + GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, + ("Failed to map stored data buffer for reading"), (NULL)); gst_buffer_unmap(out_buf, &map_out); - GST_TRACE("Store buffer unmap"); - gst_buffer_unmap(filter->last_data_buf, &map_store); - // gst_buffer_unref(data_buf); + gst_buffer_unref(out_buf); + gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); + return GST_FLOW_ERROR; + } + + GST_TRACE("Copying video buffer %lu B", map_in.size); + memcpy(map_out.data, map_in.data, map_in.size); + + GstZedSrcMeta *meta = (GstZedSrcMeta *) map_store.data; + + GST_TRACE("Adding metadata"); + gst_buffer_add_zed_src_meta(out_buf, meta->info, meta->pose, meta->sens, + meta->od_enabled, meta->obj_count, meta->objects, + meta->frame_id); + + // ----> Timestamp meta-data + GST_TRACE("Out buffer set timestamp"); + GST_BUFFER_TIMESTAMP(out_buf) = timestamp; + GST_BUFFER_DTS(out_buf) = GST_BUFFER_TIMESTAMP(out_buf); + GST_BUFFER_OFFSET(out_buf) = GST_BUFFER_OFFSET(buf); + // <---- Timestamp meta-data + + // Unmap all buffers before push (push takes ownership of out_buf) + GST_TRACE("Out buffer unmap"); + gst_buffer_unmap(out_buf, &map_out); + GST_TRACE("Store buffer unmap"); + gst_buffer_unmap(filter->last_data_buf, &map_store); + + GST_TRACE("Out buffer push"); + GstFlowReturn ret = gst_pad_push(filter->srcpad, out_buf); + // out_buf ownership transferred to downstream, do not touch it + + if (ret != GST_FLOW_OK) { + GST_DEBUG_OBJECT(filter, "Error pushing out buffer: %s", + gst_flow_get_name(ret)); + + gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); + return ret; } } else { GST_TRACE("No data buffer to be muxed"); @@ -689,14 +770,15 @@ static GstFlowReturn gst_zeddatamux_chain_video(GstPad *pad, GstObject *parent, filter->last_video_ts = timestamp; - if (!filter->last_video_buf) { - GST_TRACE("Creating new stored video buffer"); + if (!filter->last_video_buf || map_in.size != filter->last_video_buf_size) { + GST_TRACE("%s stored video buffer (size %lu)", + filter->last_video_buf ? "Reallocating" : "Creating", map_in.size); + if (filter->last_video_buf) { + gst_buffer_unref(filter->last_video_buf); + } filter->last_video_buf_size = map_in.size; filter->last_video_buf = gst_buffer_new_allocate(NULL, filter->last_video_buf_size, NULL); - } else if (map_in.size != filter->last_video_buf_size) { - filter->last_video_buf_size = map_in.size; - gst_buffer_resize(filter->last_video_buf, 0, filter->last_video_buf_size); } if (!GST_IS_BUFFER(filter->last_video_buf)) { @@ -705,7 +787,7 @@ static GstFlowReturn gst_zeddatamux_chain_video(GstPad *pad, GstObject *parent, // ----> Release incoming buffer GST_TRACE("Input buffer unmap"); gst_buffer_unmap(buf, &map_in); - // gst_buffer_unref(buf); + gst_buffer_unref(buf); // <---- Release incoming buffer return GST_FLOW_ERROR; @@ -722,9 +804,11 @@ static GstFlowReturn gst_zeddatamux_chain_video(GstPad *pad, GstObject *parent, // ----> Release incoming buffer gst_buffer_unmap(buf, &map_in); + gst_buffer_unref(buf); // <---- Release incoming buffer } else { GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, ("Failed to map buffer for reading"), (NULL)); + gst_buffer_unref(buf); return GST_FLOW_ERROR; } GST_TRACE("... processed"); diff --git a/gst-zed-demux/gstzeddemux.cpp b/gst-zed-demux/gstzeddemux.cpp index e2c986b..1242fdb 100644 --- a/gst-zed-demux/gstzeddemux.cpp +++ b/gst-zed-demux/gstzeddemux.cpp @@ -270,9 +270,12 @@ static void gst_zeddemux_set_property(GObject *object, guint prop_id, const GVal GParamSpec *pspec); static void gst_zeddemux_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); +static GstStateChangeReturn gst_zeddemux_change_state(GstElement *element, + GstStateChange transition); static gboolean gst_zeddemux_sink_event(GstPad *pad, GstObject *parent, GstEvent *event); static GstFlowReturn gst_zeddemux_chain(GstPad *pad, GstObject *parent, GstBuffer *buf); +static void gst_zeddemux_finalize(GObject *object); /* GObject vmethod implementations */ @@ -285,6 +288,9 @@ static void gst_zeddemux_class_init(GstZedDemuxClass *klass) { gobject_class->set_property = gst_zeddemux_set_property; gobject_class->get_property = gst_zeddemux_get_property; + gobject_class->finalize = gst_zeddemux_finalize; + + gstelement_class->change_state = gst_zeddemux_change_state; g_object_class_install_property(gobject_class, PROP_IS_DEPTH, g_param_spec_boolean("is-depth", "Depth", @@ -354,6 +360,52 @@ static void gst_zeddemux_init(GstZedDemux *filter) { filter->caps_aux = nullptr; } +static void gst_zeddemux_finalize(GObject *object) { + GstZedDemux *filter = GST_ZEDDEMUX(object); + + if (filter->caps_left) { + gst_caps_unref(filter->caps_left); + filter->caps_left = nullptr; + } + if (filter->caps_mono) { + gst_caps_unref(filter->caps_mono); + filter->caps_mono = nullptr; + } + if (filter->caps_aux) { + gst_caps_unref(filter->caps_aux); + filter->caps_aux = nullptr; + } + + G_OBJECT_CLASS(gst_zeddemux_parent_class)->finalize(object); +} + +static GstStateChangeReturn gst_zeddemux_change_state(GstElement *element, + GstStateChange transition) { + GstZedDemux *filter = GST_ZEDDEMUX(element); + + switch (transition) { + case GST_STATE_CHANGE_PAUSED_TO_READY: + case GST_STATE_CHANGE_READY_TO_NULL: + if (filter->caps_left) { + gst_caps_unref(filter->caps_left); + filter->caps_left = nullptr; + } + if (filter->caps_mono) { + gst_caps_unref(filter->caps_mono); + filter->caps_mono = nullptr; + } + if (filter->caps_aux) { + gst_caps_unref(filter->caps_aux); + filter->caps_aux = nullptr; + } + break; + default: + break; + } + + return GST_ELEMENT_CLASS(gst_zeddemux_parent_class)->change_state(element, transition); +} + static void gst_zeddemux_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { GstZedDemux *filter = GST_ZEDDEMUX(object); @@ -529,9 +581,7 @@ static GstFlowReturn gst_zeddemux_chain(GstPad *pad, GstObject *parent, GstBuffe GST_TRACE_OBJECT(filter, "Chain"); GstMapInfo map_in; - GstMapInfo map_out_left; - GstMapInfo map_out_mono; - GstMapInfo map_out_aux; + GstMapInfo map_out_aux; // Only needed for depth float→uint16 conversion GstMapInfo map_out_data; GstZedSrcMeta *meta = nullptr; @@ -624,11 +674,11 @@ static GstFlowReturn gst_zeddemux_chain(GstPad *pad, GstObject *parent, GstBuffe GST_TRACE("Data buffer push"); GstFlowReturn ret_data = gst_pad_push(filter->srcpad_data, data_buf); + // data_buf ownership transferred to downstream if (ret_data != GST_FLOW_OK) { GST_DEBUG_OBJECT(filter, "Error pushing data buffer: %s", gst_flow_get_name(ret_data)); - gst_buffer_unref(data_buf); /* still ours on failure */ flow_ret = ret_data; goto out; } @@ -646,133 +696,95 @@ static GstFlowReturn gst_zeddemux_chain(GstPad *pad, GstObject *parent, GstBuffe if (!filter->is_mono) { gsize left_framesize = map_in.size / 2; - GST_TRACE("Left buffer allocation - size %lu B", left_framesize); + GST_TRACE("Left buffer zero-copy region - size %lu B", left_framesize); - GstBuffer *left_proc_buf = gst_buffer_new_allocate(NULL, left_framesize, NULL); + // Zero-copy: create a sub-buffer sharing the input buffer's memory + // instead of allocating a new buffer and doing a full memcpy. + GstBuffer *left_proc_buf = gst_buffer_copy_region(buf, GST_BUFFER_COPY_MEMORY, + 0, left_framesize); if (!left_proc_buf) { GST_DEBUG("Left buffer not allocated"); flow_ret = GST_FLOW_ERROR; goto out; } - if (gst_buffer_map(left_proc_buf, &map_out_left, GST_MAP_WRITE)) { - GST_TRACE("Copying left buffer %lu B", map_out_left.size); - memcpy(map_out_left.data, map_in.data, map_out_left.size); + if (meta) { + gst_buffer_add_zed_src_meta(left_proc_buf, meta->info, meta->pose, meta->sens, + meta->od_enabled, meta->obj_count, meta->objects, + meta->frame_id); + } - if (meta) { - gst_buffer_add_zed_src_meta(left_proc_buf, meta->info, meta->pose, meta->sens, - meta->od_enabled, meta->obj_count, meta->objects, - meta->frame_id); - } + GST_TRACE("Left buffer set timestamp"); + GST_BUFFER_PTS(left_proc_buf) = GST_BUFFER_PTS(buf); + GST_BUFFER_DTS(left_proc_buf) = GST_BUFFER_DTS(buf); + GST_BUFFER_TIMESTAMP(left_proc_buf) = GST_BUFFER_TIMESTAMP(buf); - GST_TRACE("Left buffer set timestamp"); - GST_BUFFER_PTS(left_proc_buf) = GST_BUFFER_PTS(buf); - GST_BUFFER_DTS(left_proc_buf) = GST_BUFFER_DTS(buf); - GST_BUFFER_TIMESTAMP(left_proc_buf) = GST_BUFFER_TIMESTAMP(buf); + ret_left = gst_pad_push(filter->srcpad_left, left_proc_buf); + // left_proc_buf ownership transferred to downstream - /// Unmap before pushing to stream - gst_buffer_unmap(left_proc_buf, &map_out_left); - ret_left = gst_pad_push(filter->srcpad_left, left_proc_buf); - - if (ret_left != GST_FLOW_OK) { - GST_DEBUG_OBJECT(filter, "Error pushing left buffer: %s", - gst_flow_get_name(ret_left)); - gst_buffer_unref(left_proc_buf); /* still ours on failure */ - flow_ret = ret_left; - goto out; - } - // Now downstream owns left buffer - } else { - GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, ("Failed to map left buffer for writing"), - (NULL)); - gst_buffer_unref(left_proc_buf); - flow_ret = GST_FLOW_ERROR; + if (ret_left != GST_FLOW_OK) { + GST_DEBUG_OBJECT(filter, "Error pushing left buffer: %s", + gst_flow_get_name(ret_left)); + flow_ret = ret_left; goto out; } + // Now downstream owns left buffer } // <---- Left buffer // ----> Mono buffer if (filter->is_mono) { - gsize mono_framesize = map_in.size; - - GST_TRACE("Mono buffer allocation - size %lu B", mono_framesize); + GST_TRACE("Mono buffer zero-copy passthrough - size %lu B", map_in.size); - GstBuffer *mono_proc_buf = gst_buffer_new_allocate(NULL, mono_framesize, NULL); + // Zero-copy: create a sub-buffer sharing the entire input buffer's memory. + // This avoids a full-frame memcpy for mono (ZED X One) streams. + GstBuffer *mono_proc_buf = gst_buffer_copy_region(buf, GST_BUFFER_COPY_MEMORY, + 0, map_in.size); if (!mono_proc_buf) { GST_DEBUG("Mono buffer not allocated"); flow_ret = GST_FLOW_ERROR; goto out; } - if (gst_buffer_map(mono_proc_buf, &map_out_mono, GST_MAP_WRITE)) { - GST_TRACE("Copying mono buffer %lu B", map_out_mono.size); - memcpy(map_out_mono.data, map_in.data, map_out_mono.size); + if (meta) { + gst_buffer_add_zed_src_meta(mono_proc_buf, meta->info, meta->pose, meta->sens, + meta->od_enabled, meta->obj_count, meta->objects, + meta->frame_id); + } - if (meta) { - gst_buffer_add_zed_src_meta(mono_proc_buf, meta->info, meta->pose, meta->sens, - meta->od_enabled, meta->obj_count, meta->objects, - meta->frame_id); - } + GST_TRACE("Mono buffer set timestamp"); + GST_BUFFER_PTS(mono_proc_buf) = GST_BUFFER_PTS(buf); + GST_BUFFER_DTS(mono_proc_buf) = GST_BUFFER_DTS(buf); + GST_BUFFER_TIMESTAMP(mono_proc_buf) = GST_BUFFER_TIMESTAMP(buf); - GST_TRACE("Mono buffer set timestamp"); - GST_BUFFER_PTS(mono_proc_buf) = GST_BUFFER_PTS(buf); - GST_BUFFER_DTS(mono_proc_buf) = GST_BUFFER_DTS(buf); - GST_BUFFER_TIMESTAMP(mono_proc_buf) = GST_BUFFER_TIMESTAMP(buf); - - /// push before unmap - gst_buffer_unmap(mono_proc_buf, &map_out_mono); - GST_TRACE("Mono buffer push"); - ret_mono = gst_pad_push(filter->srcpad_mono, mono_proc_buf); - - if (ret_mono != GST_FLOW_OK) { - GST_DEBUG_OBJECT(filter, "Error pushing mono buffer: %s", - gst_flow_get_name(ret_mono)); - gst_buffer_unref(mono_proc_buf); - flow_ret = ret_mono; - goto out; - } - // downstream owns mono buffer - } else { - GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, ("Failed to map mono buffer for writing"), - (NULL)); - gst_buffer_unref(mono_proc_buf); - flow_ret = GST_FLOW_ERROR; + GST_TRACE("Mono buffer push"); + ret_mono = gst_pad_push(filter->srcpad_mono, mono_proc_buf); + // mono_proc_buf ownership transferred to downstream + + if (ret_mono != GST_FLOW_OK) { + GST_DEBUG_OBJECT(filter, "Error pushing mono buffer: %s", + gst_flow_get_name(ret_mono)); + flow_ret = ret_mono; goto out; } + // downstream owns mono buffer } // <---- Mono buffer // ----> Aux buffer if (!filter->is_mono) { - gsize aux_framesize = map_in.size / 2; - if (filter->is_depth) - aux_framesize /= 2; /* 16-bit data */ - - GST_TRACE("Aux buffer allocation - size %lu B", aux_framesize); - - GstBuffer *aux_proc_buf = gst_buffer_new_allocate(NULL, aux_framesize, NULL); - if (!aux_proc_buf) { - GST_DEBUG("Aux buffer not allocated"); - flow_ret = GST_FLOW_ERROR; - goto out; - } - - if (gst_buffer_map(aux_proc_buf, &map_out_aux, GST_MAP_WRITE)) { - if (!filter->is_depth) { - GST_TRACE("Copying aux buffer %lu B", map_out_aux.size); - memcpy(map_out_aux.data, map_in.data + map_in.size / 2, /* right half */ - map_out_aux.size); - } else { - GST_TRACE("Converting aux buffer %lu B", map_out_aux.size); + if (!filter->is_depth) { + // Non-depth (color) aux: zero-copy from the right half of the input + gsize aux_framesize = map_in.size / 2; - guint32 *gst_in_data = (guint32 *) (map_in.data + map_in.size / 2); - guint16 *gst_out_data = (guint16 *) map_out_aux.data; + GST_TRACE("Aux buffer zero-copy region - size %lu B", aux_framesize); - for (unsigned long i = 0; i < map_out_aux.size / sizeof(guint16); i++) { - float depth = (float) (*(gst_in_data++)); - *(gst_out_data++) = (guint16) depth; - } + GstBuffer *aux_proc_buf = gst_buffer_copy_region(buf, GST_BUFFER_COPY_MEMORY, + map_in.size / 2, aux_framesize); + if (!aux_proc_buf) { + GST_DEBUG("Aux buffer not allocated"); + flow_ret = GST_FLOW_ERROR; + goto out; } if (meta) { @@ -786,24 +798,68 @@ static GstFlowReturn gst_zeddemux_chain(GstPad *pad, GstObject *parent, GstBuffe GST_BUFFER_DTS(aux_proc_buf) = GST_BUFFER_DTS(buf); GST_BUFFER_TIMESTAMP(aux_proc_buf) = GST_BUFFER_TIMESTAMP(buf); - GST_TRACE("Aux buffer unmap"); - gst_buffer_unmap(aux_proc_buf, &map_out_aux); GST_TRACE("Aux buffer push"); ret_aux = gst_pad_push(filter->srcpad_aux, aux_proc_buf); if (ret_aux != GST_FLOW_OK) { GST_DEBUG_OBJECT(filter, "Error pushing aux buffer: %s", gst_flow_get_name(ret_aux)); - gst_buffer_unref(aux_proc_buf); flow_ret = ret_aux; goto out; } } else { - GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, ("Failed to map aux buffer for writing"), - (NULL)); - gst_buffer_unref(aux_proc_buf); - flow_ret = GST_FLOW_ERROR; - goto out; + // Depth aux: requires float→uint16 conversion, must allocate + copy + gsize aux_framesize = map_in.size / 2 / 2; /* 16-bit output from 32-bit input */ + + GST_TRACE("Aux depth buffer allocation - size %lu B", aux_framesize); + + GstBuffer *aux_proc_buf = gst_buffer_new_allocate(NULL, aux_framesize, NULL); + if (!aux_proc_buf) { + GST_DEBUG("Aux buffer not allocated"); + flow_ret = GST_FLOW_ERROR; + goto out; + } + + if (gst_buffer_map(aux_proc_buf, &map_out_aux, GST_MAP_WRITE)) { + GST_TRACE("Converting aux depth buffer %lu B", map_out_aux.size); + + guint32 *gst_in_data = (guint32 *) (map_in.data + map_in.size / 2); + guint16 *gst_out_data = (guint16 *) map_out_aux.data; + + for (unsigned long i = 0; i < map_out_aux.size / sizeof(guint16); i++) { + float depth = (float) (*(gst_in_data++)); + *(gst_out_data++) = (guint16) depth; + } + + if (meta) { + gst_buffer_add_zed_src_meta(aux_proc_buf, meta->info, meta->pose, meta->sens, + meta->od_enabled, meta->obj_count, meta->objects, + meta->frame_id); + } + + GST_TRACE("Aux buffer set timestamp"); + GST_BUFFER_PTS(aux_proc_buf) = GST_BUFFER_PTS(buf); + GST_BUFFER_DTS(aux_proc_buf) = GST_BUFFER_DTS(buf); + GST_BUFFER_TIMESTAMP(aux_proc_buf) = GST_BUFFER_TIMESTAMP(buf); + + GST_TRACE("Aux buffer unmap"); + gst_buffer_unmap(aux_proc_buf, &map_out_aux); + GST_TRACE("Aux buffer push"); + ret_aux = gst_pad_push(filter->srcpad_aux, aux_proc_buf); + + if (ret_aux != GST_FLOW_OK) { + GST_DEBUG_OBJECT(filter, "Error pushing aux buffer: %s", + gst_flow_get_name(ret_aux)); + flow_ret = ret_aux; + goto out; + } + } else { + GST_ELEMENT_ERROR(pad, RESOURCE, FAILED, ("Failed to map aux buffer for writing"), + (NULL)); + gst_buffer_unref(aux_proc_buf); + flow_ret = GST_FLOW_ERROR; + goto out; + } } } // <---- Aux buffer diff --git a/gst-zed-od-overlay/gstzedodoverlay.cpp b/gst-zed-od-overlay/gstzedodoverlay.cpp index 56aa2bb..07651aa 100644 --- a/gst-zed-od-overlay/gstzedodoverlay.cpp +++ b/gst-zed-od-overlay/gstzedodoverlay.cpp @@ -358,8 +358,14 @@ gboolean gst_zedoddisplaysink_event(GstBaseTransform *base, GstEvent *event) { gst_video_info_from_caps(&vinfo_in, caps); filter->img_left_w = vinfo_in.width; filter->img_left_h = vinfo_in.height; - if (vinfo_in.height == 752 || vinfo_in.height == 1440 || vinfo_in.height == 2160 || - vinfo_in.height == 2484) { + + // Detect composite (stacked) stereo streams by their doubled height. + // Known stereo heights: VGA=752, HD720=1440, HD1080=2160, HD1200=2400, HD2K=2484. + // Disambiguation: 3840×2160 is 4K mono (ZED X One), NOT stereo. + // HD1080 stereo is 1920×2160 — only halve when width <= 2208. + if (vinfo_in.height == 752 || vinfo_in.height == 1440 || vinfo_in.height == 2400 || + vinfo_in.height == 2484 || + (vinfo_in.height == 2160 && vinfo_in.width <= 2208)) { filter->img_left_h /= 2; // Only half buffer size if the stream is composite } @@ -384,8 +390,9 @@ static GstFlowReturn gst_zed_od_overlay_transform_ip(GstBaseTransform *base, Gst gst_object_sync_values(GST_OBJECT(filter), GST_BUFFER_TIMESTAMP(outbuf)); if (FALSE == gst_buffer_map(outbuf, &map_buf, GstMapFlags(GST_MAP_READ | GST_MAP_WRITE))) { - GST_WARNING_OBJECT(filter, "Could not map buffer for write/read"); - return GST_FLOW_OK; + GST_ELEMENT_ERROR(filter, RESOURCE, FAILED, + ("Could not map buffer for write/read"), (NULL)); + return GST_FLOW_ERROR; } // Get left image (upper half memory buffer) @@ -396,6 +403,7 @@ static GstFlowReturn gst_zed_od_overlay_transform_ip(GstBaseTransform *base, Gst if (meta == NULL) // Metadata not found { + gst_buffer_unmap(outbuf, &map_buf); GST_ELEMENT_ERROR(filter, RESOURCE, FAILED, ("No ZED metadata [GstZedSrcMeta] found in the stream'"), (NULL)); return GST_FLOW_ERROR; diff --git a/gst-zed-rtsp-server/zed-rtsp-launch.cpp b/gst-zed-rtsp-server/zed-rtsp-launch.cpp index feca32b..7173290 100644 --- a/gst-zed-rtsp-server/zed-rtsp-launch.cpp +++ b/gst-zed-rtsp-server/zed-rtsp-launch.cpp @@ -35,8 +35,10 @@ static char *port = (char *) DEFAULT_RTSP_PORT; static char *host = (char *) DEFAULT_RTSP_HOST; -static GOptionEntry entries[] = {{"port", 'p', 0, G_OPTION_ARG_STRING, &port, "Port to listen on( default: " DEFAULT_RTSP_PORT ")", "PORT"}, - {"address", 'a', 0, G_OPTION_ARG_STRING, &host, "Host address( default: " DEFAULT_RTSP_HOST ")", "HOST"}, +static GOptionEntry entries[] = {{"port", 'p', 0, G_OPTION_ARG_STRING, &port, + "Port to listen on( default: " DEFAULT_RTSP_PORT ")", "PORT"}, + {"address", 'a', 0, G_OPTION_ARG_STRING, &host, + "Host address( default: " DEFAULT_RTSP_HOST ")", "HOST"}, {NULL}}; static void client_connected(GstRTSPServer *server, GstRTSPClient *client) { @@ -55,7 +57,8 @@ int main(int argc, char *argv[]) { optctx = g_option_context_new("PIPELINE-DESCRIPTION - ZED RTSP Server, Launch\n\n" - "Example: gst-zed-rtsp-server zedsrc ! videoconvert ! 'video/x-raw, format=(string)I420' ! x264enc ! rtph264pay pt=96 name=pay0"); + "Example: gst-zed-rtsp-server zedsrc ! videoconvert ! 'video/x-raw, " + "format=(string)I420' ! x264enc ! rtph264pay pt=96 name=pay0"); g_option_context_add_main_entries(optctx, entries, NULL); g_option_context_add_group(optctx, gst_init_get_option_group()); if (!g_option_context_parse(optctx, &argc, &argv, &error)) { @@ -80,12 +83,15 @@ int main(int argc, char *argv[]) { // make a null-terminated version of argv argvn = g_new0(char *, argc); memcpy(argvn, args + 1, sizeof(char *) * (argc - 1)); - { pipeline = (GstElement *) gst_parse_launchv((const gchar **) argvn, &error); } + { + pipeline = (GstElement *) gst_parse_launchv((const gchar **) argvn, &error); + } g_free(argvn); if (!pipeline) { if (error) { - gst_printerr("ERROR - pipeline could not be constructed: %s.\n", GST_STR_NULL(error->message)); + gst_printerr("ERROR - pipeline could not be constructed: %s.\n", + GST_STR_NULL(error->message)); g_clear_error(&error); } else { gst_printerr("ERROR - pipeline could not be constructed.\n"); @@ -99,8 +105,10 @@ int main(int argc, char *argv[]) { GstElement *payload = gst_bin_get_by_name(GST_BIN(pipeline), "pay0"); if (!payload) { - gst_printerr("ERROR - at least a payload with name 'pay0' must be present in the pipeline.\n"); - gst_printerr("Example: zedsrc ! videoconvert ! video/x-raw, format=(string)I420 ! x264enc ! rtph264pay pt=96 name=pay0\n"); + gst_printerr( + "ERROR - at least a payload with name 'pay0' must be present in the pipeline.\n"); + gst_printerr("Example: zedsrc ! videoconvert ! video/x-raw, format=(string)I420 ! x264enc " + "! rtph264pay pt=96 name=pay0\n"); return 1; } g_object_unref(payload); @@ -108,7 +116,8 @@ int main(int argc, char *argv[]) { // <---- Check launch pipeline correctness */ // ----> Create RTSP Server pipeline - // Note: `gst_rtsp_media_factory_set_launch` requires a GstBin element, the easier way to create it is to enclose + // Note: `gst_rtsp_media_factory_set_launch` requires a GstBin element, the easier way to create + // it is to enclose // the pipeline in round brackets '(' ')'. std::string rtsp_pipeline; rtsp_pipeline = "( "; @@ -140,15 +149,6 @@ int main(int argc, char *argv[]) { factory = gst_rtsp_media_factory_new(); gst_rtsp_media_factory_set_launch(factory, rtsp_pipeline.c_str()); gst_rtsp_media_factory_set_shared(factory, TRUE); - - /* Don't suspend/reset the pipeline when no clients - ZED camera takes time to initialize */ - gst_rtsp_media_factory_set_suspend_mode(factory, GST_RTSP_SUSPEND_MODE_NONE); - - /* Don't send EOS when last client disconnects */ - gst_rtsp_media_factory_set_eos_shutdown(factory, FALSE); - - /* Set buffer mode to allow for camera startup latency */ - gst_rtsp_media_factory_set_latency(factory, 500); /* 500ms buffer */ /* attach the test factory to the /test url */ gst_rtsp_mount_points_add_factory(mounts, "/zed-stream", factory); @@ -161,43 +161,11 @@ int main(int argc, char *argv[]) { g_signal_connect(server, "client-connected", (GCallback) client_connected, NULL); - /* Pre-create and prepare the media so camera initializes before clients connect */ + /* start serving */ g_print(" ZED RTSP Server \n"); g_print("-----------------\n"); - g_print(" * Initializing camera (this may take a few seconds)...\n"); - - GstRTSPUrl *url = NULL; - GstRTSPResult parse_result = gst_rtsp_url_parse(("rtsp://" + std::string(host) + ":" + std::string(port) + "/zed-stream").c_str(), &url); - GstRTSPMedia *media = NULL; - if (parse_result != GST_RTSP_OK || url == NULL) { - g_printerr(" * Warning: Failed to parse RTSP URL, media will not be pre-created\n"); - } else { - media = gst_rtsp_media_factory_construct(factory, url); - gst_rtsp_url_free(url); - } - - if (media) { - GstRTSPThread *thread = gst_rtsp_thread_pool_get_thread( - gst_rtsp_server_get_thread_pool(server), - GST_RTSP_THREAD_TYPE_MEDIA, NULL); - if (gst_rtsp_media_prepare(media, thread)) { - g_print(" * Camera ready!\n"); - } else { - g_printerr(" * Warning: Failed to prepare media, clients may experience delays\n"); - g_object_unref(media); - media = NULL; - } - } - g_print(" * Stream ready at rtsp://%s:%s/zed-stream\n", host, port); - g_print("-----------------\n"); g_main_loop_run(loop); - - /* Cleanup */ - if (media) { - gst_rtsp_media_unprepare(media); - g_object_unref(media); - } return 0; } diff --git a/gst-zed-src/gstzedsrc.cpp b/gst-zed-src/gstzedsrc.cpp index db4d90d..65270fb 100644 --- a/gst-zed-src/gstzedsrc.cpp +++ b/gst-zed-src/gstzedsrc.cpp @@ -58,6 +58,7 @@ static gboolean gst_zedsrc_start(GstBaseSrc *src); static gboolean gst_zedsrc_stop(GstBaseSrc *src); static GstCaps *gst_zedsrc_get_caps(GstBaseSrc *src, GstCaps *filter); static gboolean gst_zedsrc_set_caps(GstBaseSrc *src, GstCaps *caps); +static GstCaps *gst_zedsrc_fixate(GstBaseSrc *src, GstCaps *caps); static gboolean gst_zedsrc_unlock(GstBaseSrc *src); static gboolean gst_zedsrc_unlock_stop(GstBaseSrc *src); @@ -173,7 +174,7 @@ enum { PROP_ASYNC_GRAB_CAMERA_RECOVERY, PROP_GRAB_COMPUTE_CAPPING_FPS, PROP_ENABLE_IMAGE_VALIDITY_CHECK, - PROP_ASYNC_IMAGE_RETRIEVAL, + PROP_ASYNC_IMAGE_RETRIEVAL, // Deprecated no-op, kept for backward compatibility PROP_MAX_WORKING_RES_W, PROP_MAX_WORKING_RES_H, PROP_REMOVE_SATURATED_AREAS, @@ -280,7 +281,7 @@ typedef enum { // INITIALIZATION #define DEFAULT_PROP_CAM_RES GST_ZEDSRC_AUTO_RES -#define DEFAULT_PROP_CAM_FPS GST_ZEDSRC_15FPS +#define DEFAULT_PROP_CAM_FPS GST_ZEDSRC_30FPS #define DEFAULT_PROP_SDK_VERBOSE 0 #define DEFAULT_PROP_CAM_FLIP 2 #define DEFAULT_PROP_CAM_ID 0 @@ -307,7 +308,6 @@ typedef enum { #define DEFAULT_PROP_ASYNC_GRAB_CAMERA_RECOVERY FALSE #define DEFAULT_PROP_GRAB_COMPUTE_CAPPING_FPS 0.0f #define DEFAULT_PROP_ENABLE_IMAGE_VALIDITY_CHECK TRUE -#define DEFAULT_PROP_ASYNC_IMAGE_RETRIEVAL FALSE #define DEFAULT_PROP_MAX_WORKING_RES_W 0 #define DEFAULT_PROP_MAX_WORKING_RES_H 0 #define DEFAULT_PROP_ROI FALSE @@ -786,140 +786,48 @@ static GType gst_zedsrc_3d_meas_ref_frame_get_type(void) { } /* pad templates */ +// Use ranged caps to allow flexible output resolution negotiation. +// The actual resolution is determined by the camera and downstream negotiation. +// Non-NVMM formats (BGRA, GRAY16_LE) support resize via sl::Resolution in retrieveImage(). +// NVMM formats (NV12 zero-copy) require fixed resolution (direct buffer mapping). static GstStaticPadTemplate gst_zedsrc_src_template = GST_STATIC_PAD_TEMPLATE( "src", GST_PAD_SRC, GST_PAD_ALWAYS, - GST_STATIC_CAPS(("video/x-raw, " // Double stream VGA - "format = (string)BGRA, " - "width = (int)672, " - "height = (int)752 , " - "framerate = (fraction) { 15, 30, 60, 100 }" + GST_STATIC_CAPS(("video/x-raw, " + "format = (string){BGRA, BGR, GRAY8}, " + "width = (int) [ 32, 4416 ], " + "height = (int) [ 32, 2484 ], " + "framerate = (fraction) { 15, 30, 60, 100, 120 }" ";" - "video/x-raw, " // Double stream HD720 - "format = (string)BGRA, " - "width = (int)1280, " - "height = (int)1440, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Double stream HD1080 - "format = (string)BGRA, " - "width = (int)1920, " - "height = (int)2160, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Double stream HD2K - "format = (string)BGRA, " - "width = (int)2208, " - "height = (int)2484, " - "framerate = (fraction)15" - ";" - "video/x-raw, " // Double stream HD1200 (GMSL2) - "format = (string)BGRA, " - "width = (int)1920, " - "height = (int)2400, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Double stream SVGA (GMSL2) - "format = (string)BGRA, " - "width = (int)960, " - "height = (int)1200, " - "framerate = (fraction) { 15, 30, 60, 120 }" - ";" - "video/x-raw, " // Color VGA - "format = (string)BGRA, " - "width = (int)672, " - "height = (int)376, " - "framerate = (fraction) { 15, 30, 60, 100 }" - ";" - "video/x-raw, " // Color HD720 - "format = (string)BGRA, " - "width = (int)1280, " - "height = (int)720, " - "framerate = (fraction) { 15, 30, 60}" - ";" - "video/x-raw, " // Color HD1080 - "format = (string)BGRA, " - "width = (int)1920, " - "height = (int)1080, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Color HD2K - "format = (string)BGRA, " - "width = (int)2208, " - "height = (int)1242, " - "framerate = (fraction)15" - ";" - "video/x-raw, " // Color HD1200 (GMSL2) - "format = (string)BGRA, " - "width = (int)1920, " - "height = (int)1200, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Color SVGA (GMSL2) - "format = (string)BGRA, " - "width = (int)960, " - "height = (int)600, " - "framerate = (fraction) { 15, 30, 60, 120 }" - ";" - "video/x-raw, " // Depth VGA - "format = (string)GRAY16_LE, " - "width = (int)672, " - "height = (int)376, " - "framerate = (fraction) { 15, 30, 60, 100 }" - ";" - "video/x-raw, " // Depth HD720 - "format = (string)GRAY16_LE, " - "width = (int)1280, " - "height = (int)720, " - "framerate = (fraction) { 15, 30, 60}" - ";" - "video/x-raw, " // Depth HD1080 + "video/x-raw, " "format = (string)GRAY16_LE, " - "width = (int)1920, " - "height = (int)1080, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Depth HD2K - "format = (string)GRAY16_LE, " - "width = (int)2208, " - "height = (int)1242, " - "framerate = (fraction)15" - ";" - "video/x-raw, " // Depth HD1200 (GMSL2) - "format = (string)GRAY16_LE, " - "width = (int)1920, " - "height = (int)1200, " - "framerate = (fraction) { 15, 30, 60 }" + "width = (int) [ 32, 4416 ], " + "height = (int) [ 32, 2484 ], " + "framerate = (fraction) { 15, 30, 60, 100, 120 }" +#ifdef SL_ENABLE_ADVANCED_CAPTURE_API ";" - "video/x-raw, " // Depth SVGA (GMSL2) - "format = (string)GRAY16_LE, " + "video/x-raw(memory:NVMM), " // NV12 zero-copy SVGA (GMSL2) + "format = (string)NV12, " "width = (int)960, " "height = (int)600, " "framerate = (fraction) { 15, 30, 60, 120 }" -#ifdef SL_ENABLE_ADVANCED_CAPTURE_API ";" - "video/x-raw(memory:NVMM), " // NV12 HD1200 (GMSL2 zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy HD1080 (GMSL2) "format = (string)NV12, " "width = (int)1920, " - "height = (int)1200, " + "height = (int)1080, " "framerate = (fraction) { 15, 30, 60 }" ";" - "video/x-raw(memory:NVMM), " // NV12 HD1080 (GMSL2 zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy HD1200 (GMSL2) "format = (string)NV12, " "width = (int)1920, " - "height = (int)1080, " + "height = (int)1200, " "framerate = (fraction) { 15, 30, 60 }" ";" - "video/x-raw(memory:NVMM), " // NV12 SVGA (GMSL2 zero-copy) - "format = (string)NV12, " - "width = (int)960, " - "height = (int)600, " - "framerate = (fraction) { 15, 30, 60, 120 }" - ";" - "video/x-raw(memory:NVMM), " // NV12 stereo HD1200 (side-by-side) + "video/x-raw(memory:NVMM), " // NV12 stereo side-by-side HD1200 (GMSL2) "format = (string)NV12, " "width = (int)3840, " "height = (int)1200, " - "framerate = (fraction) { 15, 30, 60 }" + "framerate = (fraction) { 15, 30 }" #endif ))); @@ -948,6 +856,7 @@ static void gst_zedsrc_class_init(GstZedSrcClass *klass) { gstbasesrc_class->stop = GST_DEBUG_FUNCPTR(gst_zedsrc_stop); gstbasesrc_class->get_caps = GST_DEBUG_FUNCPTR(gst_zedsrc_get_caps); gstbasesrc_class->set_caps = GST_DEBUG_FUNCPTR(gst_zedsrc_set_caps); + gstbasesrc_class->fixate = GST_DEBUG_FUNCPTR(gst_zedsrc_fixate); gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR(gst_zedsrc_unlock); gstbasesrc_class->unlock_stop = GST_DEBUG_FUNCPTR(gst_zedsrc_unlock_stop); gstbasesrc_class->query = GST_DEBUG_FUNCPTR(gst_zedsrc_query); @@ -1644,11 +1553,13 @@ static void gst_zedsrc_class_init(GstZedSrcClass *klass) { DEFAULT_PROP_ENABLE_IMAGE_VALIDITY_CHECK, (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + // Deprecated: kept for backward compatibility, value is ignored g_object_class_install_property( gobject_class, PROP_ASYNC_IMAGE_RETRIEVAL, - g_param_spec_boolean("async-image-retrieval", "Async Image Retrieval", - "Async Image Retrieval", DEFAULT_PROP_ASYNC_IMAGE_RETRIEVAL, - (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + g_param_spec_boolean( + "async-image-retrieval", "Async Image Retrieval (deprecated)", + "Deprecated, no longer used. Kept for backward compatibility.", FALSE, + (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_DEPRECATED))); g_object_class_install_property( gobject_class, PROP_MAX_WORKING_RES_W, @@ -1706,6 +1617,8 @@ static void gst_zedsrc_reset(GstZedSrc *src) { src->out_framesize = 0; src->is_started = FALSE; + src->stop_requested = FALSE; + src->resolved_stream_type = -1; // Reset so AUTO re-negotiates on next start src->last_frame_count = 0; src->total_dropped_frames = 0; @@ -1754,7 +1667,6 @@ static void gst_zedsrc_init(GstZedSrc *src) { src->async_grab_camera_recovery = DEFAULT_PROP_ASYNC_GRAB_CAMERA_RECOVERY; src->grab_compute_capping_fps = DEFAULT_PROP_GRAB_COMPUTE_CAPPING_FPS; src->enable_image_validity_check = DEFAULT_PROP_ENABLE_IMAGE_VALIDITY_CHECK; - src->async_image_retrieval = DEFAULT_PROP_ASYNC_IMAGE_RETRIEVAL; src->max_working_res_w = DEFAULT_PROP_MAX_WORKING_RES_W; src->max_working_res_h = DEFAULT_PROP_MAX_WORKING_RES_H; @@ -2222,7 +2134,7 @@ void gst_zedsrc_set_property(GObject *object, guint property_id, const GValue *v src->enable_image_validity_check = g_value_get_boolean(value); break; case PROP_ASYNC_IMAGE_RETRIEVAL: - src->async_image_retrieval = g_value_get_boolean(value); + // Deprecated no-op: silently accept but ignore break; case PROP_MAX_WORKING_RES_W: src->max_working_res_w = g_value_get_int(value); @@ -2277,7 +2189,7 @@ void gst_zedsrc_get_property(GObject *object, guint property_id, GValue *value, g_value_set_int(value, src->camera_id); break; case PROP_CAM_SN: - g_value_set_int64(value, src->camera_id); + g_value_set_int64(value, src->camera_sn); break; case PROP_SVO_FILE: g_value_set_string(value, src->svo_file->str); @@ -2457,7 +2369,7 @@ void gst_zedsrc_get_property(GObject *object, guint property_id, GValue *value, g_value_set_float(value, src->bt_max_range); break; case PROP_BT_KP_SELECT: - g_value_set_float(value, src->bt_kp_sel); + g_value_set_enum(value, src->bt_kp_sel); break; case PROP_BT_BODY_FITTING: g_value_set_boolean(value, src->bt_fitting); @@ -2574,7 +2486,8 @@ void gst_zedsrc_get_property(GObject *object, guint property_id, GValue *value, g_value_set_boolean(value, src->enable_image_validity_check); break; case PROP_ASYNC_IMAGE_RETRIEVAL: - g_value_set_boolean(value, src->async_image_retrieval); + // Deprecated no-op: always return FALSE + g_value_set_boolean(value, FALSE); break; case PROP_MAX_WORKING_RES_W: g_value_set_int(value, src->max_working_res_w); @@ -2624,6 +2537,13 @@ void gst_zedsrc_finalize(GObject *object) { GST_TRACE_OBJECT(src, "gst_zedsrc_finalize"); + /* Ensure camera is closed even if stop() was never called + * (e.g. abnormal shutdown, signal interruption) */ + if (src->zed.isOpened()) { + GST_WARNING_OBJECT(src, "Camera still open in finalize — forcing close"); + src->zed.close(); + } + /* clean up object here */ if (src->caps) { gst_caps_unref(src->caps); @@ -2695,6 +2615,176 @@ static gboolean gst_zedsrc_downstream_accepts_nv12_nvmm(GstZedSrc *src) { return accepts_nv12_nvmm; } +/* Camera mode: maps GstZedSrcRes enum to sl::RESOLUTION. + * Resolution dimensions come from sl::getResolution(). + * FPS validity is queried dynamically via sl::isAvailable(fps, resolution, model). */ +typedef struct { + GstZedSrcRes resolution; + sl::RESOLUTION sl_resolution; +} ZedCameraMode; + +/* Camera modes ordered by quality (highest first) for AUTO resolution selection */ +static const ZedCameraMode zed_camera_modes[] = { + {GST_ZEDSRC_HD2K, sl::RESOLUTION::HD2K}, {GST_ZEDSRC_HD1200, sl::RESOLUTION::HD1200}, + {GST_ZEDSRC_HD1080, sl::RESOLUTION::HD1080}, {GST_ZEDSRC_HD720, sl::RESOLUTION::HD720}, + {GST_ZEDSRC_SVGA, sl::RESOLUTION::SVGA}, {GST_ZEDSRC_VGA, sl::RESOLUTION::VGA}, +}; +static const guint zed_camera_modes_count = G_N_ELEMENTS(zed_camera_modes); + +/* + * Detect the camera model using sl::Camera::getDeviceList() (lightweight, no open needed). + * If camera_id or camera_sn is set, match the specific device; otherwise use the first one. + */ +static sl::MODEL gst_zedsrc_detect_camera_model(GstZedSrc *src) { + auto devices = sl::Camera::getDeviceList(); + if (devices.empty()) { + GST_ERROR_OBJECT(src, "No ZED cameras detected"); + return sl::MODEL::LAST; + } + + sl::MODEL model = devices[0].camera_model; + + /* Match specific camera if user set camera-id or camera-sn */ + if (src->camera_id != 0) { + for (const auto &dev : devices) { + if (dev.id == static_cast(src->camera_id)) { + model = dev.camera_model; + break; + } + } + } else if (src->camera_sn != 0) { + for (const auto &dev : devices) { + if (dev.serial_number == static_cast(src->camera_sn)) { + model = dev.camera_model; + break; + } + } + } + + GST_INFO("Detected camera model: %s", sl::toString(model).c_str()); + return model; +} + +/* Look up a camera mode by resolution enum */ +static const ZedCameraMode *gst_zedsrc_get_camera_mode(GstZedSrcRes resolution) { + for (guint i = 0; i < zed_camera_modes_count; ++i) { + if (zed_camera_modes[i].resolution == resolution) { + return &zed_camera_modes[i]; + } + } + return NULL; +} + +/* + * Query the SDK for valid FPS values for a given resolution + camera model. + * Returns the count of valid FPS values written to out_fps (up to max_count). + */ +static guint gst_zedsrc_get_valid_fps(sl::RESOLUTION sl_res, sl::MODEL model, gint *out_fps, + guint max_count) { + const auto &candidate_fps = sl::getAvailableCameraFPS(); + guint count = 0; + for (size_t i = 0; i < candidate_fps.size() && count < max_count; ++i) { + if (sl::isAvailable(candidate_fps[i], sl_res, model)) { + out_fps[count++] = candidate_fps[i]; + } + } + return count; +} + +/* Return the closest valid FPS for the current camera resolution + detected model */ +static gint gst_zedsrc_select_fps(GstZedSrc *src, gint requested_fps) { + if (requested_fps <= 0) { + requested_fps = DEFAULT_PROP_CAM_FPS; + } + + const ZedCameraMode *mode = + gst_zedsrc_get_camera_mode(static_cast(src->camera_resolution)); + if (!mode) { + return requested_fps; + } + + sl::MODEL camera_model = gst_zedsrc_detect_camera_model(src); + gint valid_fps[8]; + guint count = gst_zedsrc_get_valid_fps(mode->sl_resolution, camera_model, valid_fps, 8); + if (count == 0) { + return requested_fps; + } + + gint best = valid_fps[0]; + gint best_diff = ABS(requested_fps - best); + for (guint i = 1; i < count; ++i) { + gint diff = ABS(requested_fps - valid_fps[i]); + if (diff < best_diff) { + best = valid_fps[i]; + best_diff = diff; + } + } + + return best; +} + +/* + * Resolve camera_resolution when set to AUTO. + * + * Strategy: detect the camera model using sl::Camera::getDeviceList() + * and use sl::isAvailable(resolution, model) to filter compatible modes. + * Then pick the highest-quality resolution that supports the requested FPS. + * + * If the requested FPS doesn't exactly match any mode, the nearest valid + * FPS is selected first, then the best resolution for that FPS is chosen. + */ +static void gst_zedsrc_resolve_auto_resolution(GstZedSrc *src) { + gint requested_fps = src->camera_fps; + if (requested_fps <= 0) { + requested_fps = DEFAULT_PROP_CAM_FPS; + } + + /* Detect camera model to filter incompatible resolutions */ + sl::MODEL camera_model = gst_zedsrc_detect_camera_model(src); + if (camera_model == sl::MODEL::LAST) { + GST_ERROR_OBJECT(src, "Cannot resolve AUTO resolution: no camera detected"); + return; + } + + /* First: find the closest valid FPS across compatible modes */ + const auto &candidate_fps = sl::getAvailableCameraFPS(); + gint best_fps = requested_fps; + gint best_fps_diff = G_MAXINT; + for (guint i = 0; i < zed_camera_modes_count; ++i) { + if (!sl::isAvailable(zed_camera_modes[i].sl_resolution, camera_model)) + continue; + for (size_t j = 0; j < candidate_fps.size(); ++j) { + if (!sl::isAvailable(candidate_fps[j], zed_camera_modes[i].sl_resolution, camera_model)) + continue; + gint diff = ABS(requested_fps - candidate_fps[j]); + if (diff < best_fps_diff) { + best_fps_diff = diff; + best_fps = candidate_fps[j]; + } + } + } + + /* Second: pick the highest quality compatible resolution supporting that FPS */ + for (guint i = 0; i < zed_camera_modes_count; ++i) { + if (!sl::isAvailable(zed_camera_modes[i].sl_resolution, camera_model)) + continue; + if (sl::isAvailable(best_fps, zed_camera_modes[i].sl_resolution, camera_model)) { + src->camera_resolution = zed_camera_modes[i].resolution; + src->camera_fps = best_fps; + sl::Resolution native = sl::getResolution(zed_camera_modes[i].sl_resolution); + GST_INFO("AUTO resolution resolved: %dx%d @ %d FPS (requested %d FPS, model %s)", + (int) native.width, (int) native.height, best_fps, requested_fps, + sl::toString(camera_model).c_str()); + return; + } + } + + /* Fallback (shouldn't happen): HD1080@30 — works on all camera types */ + src->camera_resolution = GST_ZEDSRC_HD1080; + src->camera_fps = 30; + GST_WARNING("AUTO resolution fallback to HD1080@30"); +} + /* Resolve stream-type=AUTO to actual stream type based on downstream caps and SDK capability */ static void gst_zedsrc_resolve_stream_type(GstZedSrc *src) { // If user explicitly set a stream type (not AUTO), use that @@ -2737,6 +2827,9 @@ static void gst_zedsrc_resolve_stream_type(GstZedSrc *src) { src->resolved_stream_type); } +// Minimum output resolution for ranged caps (values below this are not practical) +#define GST_ZEDSRC_MIN_OUTPUT_SIZE 32 + static gboolean gst_zedsrc_calculate_caps(GstZedSrc *src) { GST_TRACE_OBJECT(src, "gst_zedsrc_calculate_caps"); @@ -2744,23 +2837,42 @@ static gboolean gst_zedsrc_calculate_caps(GstZedSrc *src) { gint stream_type = src->resolved_stream_type; guint32 width, height; - gint fps; GstVideoInfo vinfo; - GstVideoFormat format = GST_VIDEO_FORMAT_BGRA; - + GstVideoFormat format = GST_VIDEO_FORMAT_UNKNOWN; + const gchar *format_str = NULL; + gboolean use_ranged_caps = TRUE; // Allow flexible output resolution + gboolean multi_format = FALSE; // Advertise multiple formats (BGRA, BGR, GRAY8) + + // Format is determined by stream_type: + // - DEPTH_16 → GRAY16_LE (fixed, it's a measure) + // - NV12 zero-copy → NV12 (fixed, direct buffer mapping) + // - Image stream types → multi-format (BGRA, BGR, GRAY8) negotiated with downstream if (stream_type == GST_ZEDSRC_DEPTH_16) { format = GST_VIDEO_FORMAT_GRAY16_LE; + format_str = "GRAY16_LE"; } #ifdef SL_ENABLE_ADVANCED_CAPTURE_API else if (stream_type == GST_ZEDSRC_RAW_NV12 || stream_type == GST_ZEDSRC_RAW_NV12_STEREO) { format = GST_VIDEO_FORMAT_NV12; + format_str = "NV12"; + use_ranged_caps = FALSE; // Zero-copy modes can't resize } #endif + else { + // Image stream types support BGRA, BGR, GRAY8 via SDK VIEW variants + format = GST_VIDEO_FORMAT_BGRA; // Default/preferred format + format_str = "BGRA"; + multi_format = TRUE; + } sl::CameraInformation cam_info = src->zed.getCameraInformation(); - width = cam_info.camera_configuration.resolution.width; - height = cam_info.camera_configuration.resolution.height; + // Store camera native resolution + src->camera_width = cam_info.camera_configuration.resolution.width; + src->camera_height = cam_info.camera_configuration.resolution.height; + + width = src->camera_width; + height = src->camera_height; if (stream_type == GST_ZEDSRC_LEFT_RIGHT || stream_type == GST_ZEDSRC_LEFT_DEPTH) { height *= 2; @@ -2773,18 +2885,94 @@ static gboolean gst_zedsrc_calculate_caps(GstZedSrc *src) { } #endif - fps = static_cast(cam_info.camera_configuration.fps); + // Get the mode for the resolved camera resolution + const ZedCameraMode *mode = + gst_zedsrc_get_camera_mode(static_cast(src->camera_resolution)); + gint fps = src->camera_fps; // Already clamped in start() + + // Query valid FPS from SDK for this resolution + camera model + sl::MODEL caps_model = cam_info.camera_model; + gint valid_fps[8]; + guint valid_fps_count = 0; + if (mode) { + valid_fps_count = gst_zedsrc_get_valid_fps(mode->sl_resolution, caps_model, valid_fps, 8); + } + + if (src->caps) { + gst_caps_unref(src->caps); + src->caps = NULL; + } if (format != GST_VIDEO_FORMAT_UNKNOWN) { + if (use_ranged_caps && width >= GST_ZEDSRC_MIN_OUTPUT_SIZE && + height >= GST_ZEDSRC_MIN_OUTPUT_SIZE) { + // Create caps with ranged width/height for flexible output resolution. + // Downstream can negotiate any size from [32, max] and we'll use + // sl::Resolution in retrieveImage()/retrieveMeasure() to resize. + src->caps = gst_caps_new_empty_simple("video/x-raw"); + GstStructure *s = gst_caps_get_structure(src->caps, 0); + + // Set format: multi-format list (BGRA preferred) or single format + if (multi_format) { + GValue fmt_list = G_VALUE_INIT; + g_value_init(&fmt_list, GST_TYPE_LIST); + const gchar *formats[] = {"BGRA", "BGR", "GRAY8"}; + for (guint i = 0; i < G_N_ELEMENTS(formats); ++i) { + GValue v = G_VALUE_INIT; + g_value_init(&v, G_TYPE_STRING); + g_value_set_string(&v, formats[i]); + gst_value_list_append_value(&fmt_list, &v); + g_value_unset(&v); + } + gst_structure_set_value(s, "format", &fmt_list); + g_value_unset(&fmt_list); + } else { + gst_structure_set(s, "format", G_TYPE_STRING, format_str, NULL); + } + + // Set ranged width/height + gst_structure_set(s, "width", GST_TYPE_INT_RANGE, GST_ZEDSRC_MIN_OUTPUT_SIZE, + (gint) width, "height", GST_TYPE_INT_RANGE, + GST_ZEDSRC_MIN_OUTPUT_SIZE, (gint) height, NULL); + + // Set framerate as a list of valid values for this camera resolution + if (valid_fps_count > 1) { + GValue fps_list = G_VALUE_INIT; + g_value_init(&fps_list, GST_TYPE_LIST); + for (guint i = 0; i < valid_fps_count; ++i) { + GValue fps_val = G_VALUE_INIT; + g_value_init(&fps_val, GST_TYPE_FRACTION); + gst_value_set_fraction(&fps_val, valid_fps[i], 1); + gst_value_list_append_value(&fps_list, &fps_val); + g_value_unset(&fps_val); + } + gst_structure_set_value(s, "framerate", &fps_list); + g_value_unset(&fps_list); + } else { + gst_structure_set(s, "framerate", GST_TYPE_FRACTION, fps, 1, NULL); + } + + GST_INFO_OBJECT(src, "Created ranged caps: format=%s%s, width=[%d,%d], height=[%d,%d]", + format_str, multi_format ? " (+BGR, GRAY8)" : "", + GST_ZEDSRC_MIN_OUTPUT_SIZE, width, GST_ZEDSRC_MIN_OUTPUT_SIZE, height); + } else { + // Fixed caps for zero-copy modes or when ranged caps not applicable + gst_video_info_init(&vinfo); + gst_video_info_set_format(&vinfo, format, width, height); + vinfo.fps_n = fps; + vinfo.fps_d = 1; + src->caps = gst_video_info_to_caps(&vinfo); + } + + // Calculate frame size based on maximum resolution gst_video_info_init(&vinfo); gst_video_info_set_format(&vinfo, format, width, height); - if (src->caps) { - gst_caps_unref(src->caps); - } src->out_framesize = (guint) GST_VIDEO_INFO_SIZE(&vinfo); - vinfo.fps_n = fps; - vinfo.fps_d = 1; - src->caps = gst_video_info_to_caps(&vinfo); + + // Initialize output resolution and format (will be updated in set_caps) + src->output_width = width; + src->output_height = height; + src->output_format = format; #ifdef SL_ENABLE_ADVANCED_CAPTURE_API // Add memory:NVMM feature for zero-copy NV12 modes @@ -2797,7 +2985,12 @@ static gboolean gst_zedsrc_calculate_caps(GstZedSrc *src) { } gst_base_src_set_blocksize(GST_BASE_SRC(src), src->out_framesize); - gst_base_src_set_caps(GST_BASE_SRC(src), src->caps); + + // Only call gst_base_src_set_caps for fixed caps (non-ranged) + // For ranged caps, negotiation happens via get_caps/set_caps + if (!use_ranged_caps) { + gst_base_src_set_caps(GST_BASE_SRC(src), src->caps); + } GST_DEBUG_OBJECT(src, "Created caps %" GST_PTR_FORMAT, src->caps); return TRUE; @@ -2813,36 +3006,27 @@ static gboolean gst_zedsrc_start(GstBaseSrc *bsrc) { return FALSE; #endif - GST_TRACE_OBJECT(src, "gst_zedsrc_calculate_caps"); + GST_TRACE_OBJECT(src, "gst_zedsrc_start"); + + // ----> Resolve AUTO camera resolution based on requested FPS + if (src->camera_resolution == GST_ZEDSRC_AUTO_RES) { + gst_zedsrc_resolve_auto_resolution(src); + } else { + // Explicit resolution: clamp FPS to nearest valid for this resolution + src->camera_fps = gst_zedsrc_select_fps(src, src->camera_fps); + GST_INFO("Explicit resolution: clamped FPS to %d", src->camera_fps); + } // ----> Set init parameters sl::InitParameters init_params; GST_INFO("CAMERA INITIALIZATION PARAMETERS"); - switch (src->camera_resolution) { - case GST_ZEDSRC_HD2K: - init_params.camera_resolution = sl::RESOLUTION::HD2K; - break; - case GST_ZEDSRC_HD1080: - init_params.camera_resolution = sl::RESOLUTION::HD1080; - break; - case GST_ZEDSRC_HD1200: - init_params.camera_resolution = sl::RESOLUTION::HD1200; - break; - case GST_ZEDSRC_HD720: - init_params.camera_resolution = sl::RESOLUTION::HD720; - break; - case GST_ZEDSRC_SVGA: - init_params.camera_resolution = sl::RESOLUTION::SVGA; - break; - case GST_ZEDSRC_VGA: - init_params.camera_resolution = sl::RESOLUTION::VGA; - break; - case GST_ZEDSRC_AUTO_RES: - init_params.camera_resolution = sl::RESOLUTION::AUTO; - break; - default: + const ZedCameraMode *resolved_mode = + gst_zedsrc_get_camera_mode(static_cast(src->camera_resolution)); + if (resolved_mode) { + init_params.camera_resolution = resolved_mode->sl_resolution; + } else { GST_ELEMENT_ERROR(src, RESOURCE, NOT_FOUND, ("Failed to set camera resolution"), (NULL)); return FALSE; } @@ -2922,9 +3106,6 @@ static gboolean gst_zedsrc_start(GstBaseSrc *bsrc) { GST_INFO(" * Grab Compute Capping FPS: %g", init_params.grab_compute_capping_fps); init_params.enable_image_validity_check = src->enable_image_validity_check; GST_INFO(" * Enable Image Validity Check: %d", init_params.enable_image_validity_check); - init_params.async_image_retrieval = src->async_image_retrieval == TRUE; - GST_INFO(" * Async Image Retrieval: %s", - (init_params.async_image_retrieval ? "TRUE" : "FALSE")); if (src->max_working_res_w > 0 && src->max_working_res_h > 0) { init_params.maximum_working_resolution = sl::Resolution(src->max_working_res_w, src->max_working_res_h); @@ -3317,6 +3498,58 @@ static gboolean gst_zedsrc_set_caps(GstBaseSrc *bsrc, GstCaps *caps) { goto unsupported_caps; } + // Capture the negotiated output resolution and format + src->output_width = GST_VIDEO_INFO_WIDTH(&vinfo); + src->output_height = GST_VIDEO_INFO_HEIGHT(&vinfo); + src->output_format = GST_VIDEO_INFO_FORMAT(&vinfo); + + GST_INFO_OBJECT(src, "Negotiated format: %s", gst_video_format_to_string(src->output_format)); + + // Validate negotiated sizes for stereo top-bottom modes + if (src->resolved_stream_type == GST_ZEDSRC_LEFT_RIGHT || + src->resolved_stream_type == GST_ZEDSRC_LEFT_DEPTH) { + if ((src->output_height % 2) != 0) { + GST_ERROR_OBJECT(src, "Invalid negotiated height %d for top-bottom stereo", + src->output_height); + return FALSE; + } + } +#ifdef SL_ENABLE_ADVANCED_CAPTURE_API + // Validate NV12 zero-copy sizes: the negotiated resolution must match the camera + // native resolution exactly, since zero-copy maps the raw buffer directly. + if (src->resolved_stream_type == GST_ZEDSRC_RAW_NV12 || + src->resolved_stream_type == GST_ZEDSRC_RAW_NV12_STEREO) { + const gint width = src->output_width; + const gint height = src->output_height; + const guint cam_w = src->camera_width; + const guint cam_h = src->camera_height; + gboolean valid_size = FALSE; + + if (src->resolved_stream_type == GST_ZEDSRC_RAW_NV12_STEREO) { + // Stereo side-by-side: width = 2 * cam_w, height = cam_h + valid_size = (width == (gint) (cam_w * 2) && height == (gint) cam_h); + } else { + valid_size = (width == (gint) cam_w && height == (gint) cam_h); + } + + if (!valid_size) { + GST_ERROR_OBJECT(src, "Invalid negotiated NV12 size %dx%d (expected %ux%u for %s)", + width, height, cam_w, cam_h, + src->resolved_stream_type == GST_ZEDSRC_RAW_NV12_STEREO ? "stereo" + : "mono"); + return FALSE; + } + } +#endif + + // Recalculate frame size based on negotiated resolution + src->out_framesize = (guint) GST_VIDEO_INFO_SIZE(&vinfo); + gst_base_src_set_blocksize(GST_BASE_SRC(src), src->out_framesize); + + GST_INFO_OBJECT(src, "Negotiated output resolution: %dx%d (camera: %dx%d), framesize: %u", + src->output_width, src->output_height, src->camera_width, src->camera_height, + src->out_framesize); + return TRUE; unsupported_caps: @@ -3324,6 +3557,44 @@ static gboolean gst_zedsrc_set_caps(GstBaseSrc *bsrc, GstCaps *caps) { return FALSE; } +static GstCaps *gst_zedsrc_fixate(GstBaseSrc *bsrc, GstCaps *caps) { + GstZedSrc *src = GST_ZED_SRC(bsrc); + GstStructure *structure; + + GST_DEBUG_OBJECT(src, "Fixating caps %" GST_PTR_FORMAT, caps); + + caps = gst_caps_make_writable(caps); + structure = gst_caps_get_structure(caps, 0); + + // Default output to camera native resolution when downstream doesn't constrain. + // output_width/height are always initialized in start() before fixate is called. + gst_structure_fixate_field_nearest_int(structure, "width", src->output_width); + gst_structure_fixate_field_nearest_int(structure, "height", src->output_height); + + // Default framerate to the camera_fps (already clamped in start()). + // If downstream constrained the FPS list, pick the closest valid one. + { + gint fps_n = 0, fps_d = 1; + gint requested_fps = src->camera_fps; + + if (gst_structure_get_fraction(structure, "framerate", &fps_n, &fps_d) && fps_d > 0) { + requested_fps = (gint) ((gdouble) fps_n / (gdouble) fps_d + 0.5); + } + + const gint selected_fps = gst_zedsrc_select_fps(src, requested_fps); + src->camera_fps = selected_fps; + gst_structure_set(structure, "framerate", GST_TYPE_FRACTION, selected_fps, 1, NULL); + GST_INFO_OBJECT(src, "Fixated: %dx%d @ %d FPS (camera: %dx%d)", src->output_width, + src->output_height, selected_fps, src->camera_width, src->camera_height); + } + + // Chain up to parent fixate + caps = GST_BASE_SRC_CLASS(gst_zedsrc_parent_class)->fixate(bsrc, caps); + + GST_DEBUG_OBJECT(src, "Fixated caps %" GST_PTR_FORMAT, caps); + return caps; +} + static gboolean gst_zedsrc_unlock(GstBaseSrc *bsrc) { GstZedSrc *src = GST_ZED_SRC(bsrc); @@ -3856,34 +4127,90 @@ static GstFlowReturn gst_zedsrc_fill(GstPushSrc *psrc, GstBuffer *buf) { // NOTE: NV12 zero-copy modes (GST_ZEDSRC_RAW_NV12, GST_ZEDSRC_RAW_NV12_STEREO) are handled // by gst_zedsrc_create() which wraps NvBufSurface directly without memcpy. // This fill() function only handles non-NVMM stream types. - if (stream_type == GST_ZEDSRC_ONLY_LEFT) { - CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, sl::VIEW::LEFT, sl::MEM::CPU)); - } else if (stream_type == GST_ZEDSRC_ONLY_RIGHT) { - CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, sl::VIEW::RIGHT, sl::MEM::CPU)); - } else if (stream_type == GST_ZEDSRC_LEFT_RIGHT) { - CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, sl::VIEW::LEFT, sl::MEM::CPU)); - CHECK_RET_OR_GOTO(src->zed.retrieveImage(right_img, sl::VIEW::RIGHT, sl::MEM::CPU)); - } else if (stream_type == GST_ZEDSRC_LEFT_RIGHT_SBS) { - CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, sl::VIEW::SIDE_BY_SIDE, sl::MEM::CPU)); - } else if (stream_type == GST_ZEDSRC_DEPTH_16) { - CHECK_RET_OR_GOTO( - src->zed.retrieveMeasure(depth_data, sl::MEASURE::DEPTH_U16_MM, sl::MEM::CPU)); - } else if (stream_type == GST_ZEDSRC_LEFT_DEPTH) { - CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, sl::VIEW::LEFT, sl::MEM::CPU)); - CHECK_RET_OR_GOTO(src->zed.retrieveMeasure(depth_data, sl::MEASURE::DEPTH, sl::MEM::CPU)); + // + // Calculate the sl::Resolution for retrieveImage()/retrieveMeasure() based on negotiated output + // For stereo modes (LEFT_RIGHT, LEFT_DEPTH), each individual image is half the output height + // For side-by-side mode (LEFT_RIGHT_SBS), the SDK returns combined image directly + { + sl::Resolution out_res; + + if (stream_type == GST_ZEDSRC_LEFT_RIGHT || stream_type == GST_ZEDSRC_LEFT_DEPTH) { + // Top-bottom stereo: each image is half the output height + out_res = sl::Resolution(src->output_width, src->output_height / 2); + } else if (stream_type == GST_ZEDSRC_LEFT_RIGHT_SBS) { + // Side-by-side: SDK returns combined image at output_width x output_height + out_res = sl::Resolution(src->output_width, src->output_height); + } else { + // Single view modes + out_res = sl::Resolution(src->output_width, src->output_height); + } + + // Select sl::VIEW based on negotiated format and stream type. + // SDK VIEW variants: LEFT/RIGHT/SIDE_BY_SIDE (BGRA), _BGR (BGR), _GRAY (GRAY8) + const GstVideoFormat fmt = src->output_format; + + if (stream_type == GST_ZEDSRC_ONLY_LEFT) { + sl::VIEW view = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::LEFT_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::LEFT_GRAY + : sl::VIEW::LEFT; + CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, view, sl::MEM::CPU, out_res)); + } else if (stream_type == GST_ZEDSRC_ONLY_RIGHT) { + sl::VIEW view = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::RIGHT_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::RIGHT_GRAY + : sl::VIEW::RIGHT; + CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, view, sl::MEM::CPU, out_res)); + } else if (stream_type == GST_ZEDSRC_LEFT_RIGHT) { + sl::VIEW lview = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::LEFT_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::LEFT_GRAY + : sl::VIEW::LEFT; + sl::VIEW rview = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::RIGHT_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::RIGHT_GRAY + : sl::VIEW::RIGHT; + CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, lview, sl::MEM::CPU, out_res)); + CHECK_RET_OR_GOTO(src->zed.retrieveImage(right_img, rview, sl::MEM::CPU, out_res)); + } else if (stream_type == GST_ZEDSRC_LEFT_RIGHT_SBS) { + sl::VIEW view = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::SIDE_BY_SIDE_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::SIDE_BY_SIDE_GRAY + : sl::VIEW::SIDE_BY_SIDE; + CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, view, sl::MEM::CPU, out_res)); + } else if (stream_type == GST_ZEDSRC_DEPTH_16) { + CHECK_RET_OR_GOTO(src->zed.retrieveMeasure(depth_data, sl::MEASURE::DEPTH_U16_MM, + sl::MEM::CPU, out_res)); + } else if (stream_type == GST_ZEDSRC_LEFT_DEPTH) { + sl::VIEW lview = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::LEFT_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::LEFT_GRAY + : sl::VIEW::LEFT; + CHECK_RET_OR_GOTO(src->zed.retrieveImage(left_img, lview, sl::MEM::CPU, out_res)); + CHECK_RET_OR_GOTO( + src->zed.retrieveMeasure(depth_data, sl::MEASURE::DEPTH, sl::MEM::CPU, out_res)); + } } + // <---- Mats retrieving /* --- Memory copy into GstBuffer ------------------------------------ */ if (stream_type == GST_ZEDSRC_DEPTH_16) { memcpy(minfo.data, depth_data.getPtr(), minfo.size); } else if (stream_type == GST_ZEDSRC_LEFT_RIGHT) { - /* Left RGB data on half top */ - memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); - /* Right RGB data on half bottom */ - memcpy(minfo.data + minfo.size / 2, right_img.getPtr(), minfo.size / 2); + /* Left data on half top, Right data on half bottom */ + if (src->output_format == GST_VIDEO_FORMAT_BGR) { + memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + memcpy(minfo.data + minfo.size / 2, right_img.getPtr(), minfo.size / 2); + } else if (src->output_format == GST_VIDEO_FORMAT_GRAY8) { + memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + memcpy(minfo.data + minfo.size / 2, right_img.getPtr(), minfo.size / 2); + } else { + memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + memcpy(minfo.data + minfo.size / 2, right_img.getPtr(), minfo.size / 2); + } } else if (stream_type == GST_ZEDSRC_LEFT_DEPTH) { - /* RGB data on half top */ - memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + /* Image data on half top */ + if (src->output_format == GST_VIDEO_FORMAT_BGR) { + memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + } else if (src->output_format == GST_VIDEO_FORMAT_GRAY8) { + memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + } else { + memcpy(minfo.data, left_img.getPtr(), minfo.size / 2); + } /* Depth data on half bottom */ { @@ -3895,7 +4222,14 @@ static GstFlowReturn gst_zedsrc_fill(GstPushSrc *psrc, GstBuffer *buf) { } } } else { - memcpy(minfo.data, left_img.getPtr(), minfo.size); + /* Single view (ONLY_LEFT, LEFT_RIGHT_SBS) */ + if (src->output_format == GST_VIDEO_FORMAT_BGR) { + memcpy(minfo.data, left_img.getPtr(), minfo.size); + } else if (src->output_format == GST_VIDEO_FORMAT_GRAY8) { + memcpy(minfo.data, left_img.getPtr(), minfo.size); + } else { + memcpy(minfo.data, left_img.getPtr(), minfo.size); + } } // <---- Memory copy diff --git a/gst-zed-src/gstzedsrc.h b/gst-zed-src/gstzedsrc.h index 97bff19..2e92c7f 100644 --- a/gst-zed-src/gstzedsrc.h +++ b/gst-zed-src/gstzedsrc.h @@ -23,6 +23,7 @@ #define _GST_ZED_SRC_H_ #include +#include #include "sl/Camera.hpp" @@ -80,7 +81,6 @@ struct _GstZedSrc { gboolean async_grab_camera_recovery; gfloat grab_compute_capping_fps; gboolean enable_image_validity_check; - gboolean async_image_retrieval; gint max_working_res_w; gint max_working_res_h; @@ -185,6 +185,15 @@ struct _GstZedSrc { GstCaps *caps; guint out_framesize; + // Camera native resolution (set after open) + guint camera_width; + guint camera_height; + + // Output resolution and format (negotiated via caps, used for retrieveImage) + guint output_width; + guint output_height; + GstVideoFormat output_format; // Negotiated pixel format (BGRA, BGR, GRAY8...) + gboolean stop_requested; }; diff --git a/gst-zedxone-src/gstzedxonesrc.cpp b/gst-zedxone-src/gstzedxonesrc.cpp index 57a32e2..b0fa397 100644 --- a/gst-zedxone-src/gstzedxonesrc.cpp +++ b/gst-zedxone-src/gstzedxonesrc.cpp @@ -35,6 +35,9 @@ #include +// Minimum output resolution for ranged caps (allows downstream to negotiate any size >= 32) +#define GST_ZEDXONESRC_MIN_OUTPUT_SIZE 32 + GST_DEBUG_CATEGORY_STATIC(gst_zedxonesrc_debug); #define GST_CAT_DEFAULT gst_zedxonesrc_debug @@ -50,6 +53,7 @@ static gboolean gst_zedxonesrc_start(GstBaseSrc *src); static gboolean gst_zedxonesrc_stop(GstBaseSrc *src); static GstCaps *gst_zedxonesrc_get_caps(GstBaseSrc *src, GstCaps *filter); static gboolean gst_zedxonesrc_set_caps(GstBaseSrc *src, GstCaps *caps); +static GstCaps *gst_zedxonesrc_fixate(GstBaseSrc *src, GstCaps *caps); static gboolean gst_zedxonesrc_unlock(GstBaseSrc *src); static gboolean gst_zedxonesrc_unlock_stop(GstBaseSrc *src); static gboolean gst_zedxonesrc_query(GstBaseSrc *src, GstQuery *query); @@ -114,11 +118,12 @@ typedef enum { } GstZedXOneSrcStreamType; typedef enum { - GST_ZEDXONESRC_SVGA, // 960 x 600 - GST_ZEDXONESRC_1080P, // 1920 x 1080 - GST_ZEDXONESRC_1200P, // 1920 x 1200 - GST_ZEDXONESRC_QHDPLUS, // 3200x1800 - GST_ZEDXONESRC_4K // 3840 x 2160 + GST_ZEDXONESRC_SVGA, // 960 x 600 + GST_ZEDXONESRC_1080P, // 1920 x 1080 + GST_ZEDXONESRC_1200P, // 1920 x 1200 + GST_ZEDXONESRC_QHDPLUS, // 3200x1800 + GST_ZEDXONESRC_4K, // 3840 x 2160 + GST_ZEDXONESRC_AUTO_RES, // Auto: best resolution for requested FPS } GstZedXOneSrcRes; typedef enum { @@ -148,7 +153,7 @@ typedef enum { //////////////// DEFAULT PARAMETERS ///////////////////////////////////////////////////////////////////////////// -#define DEFAULT_PROP_CAM_RES GST_ZEDXONESRC_1200P +#define DEFAULT_PROP_CAM_RES GST_ZEDXONESRC_AUTO_RES #define DEFAULT_PROP_CAM_FPS GST_ZEDXONESRC_30FPS #define DEFAULT_PROP_VERBOSE_LVL 1 #define DEFAULT_PROP_TIMEOUT_SEC 5.0f @@ -223,6 +228,7 @@ static GType gst_zedxonesrc_resol_get_type(void) { {static_cast(GST_ZEDXONESRC_1200P), "1920x1200", "HD1200"}, {static_cast(GST_ZEDXONESRC_1080P), "1920x1080", "HD1080"}, {static_cast(GST_ZEDXONESRC_SVGA), "960x600 (only ZED X One GS)", "SVGA"}, + {static_cast(GST_ZEDXONESRC_AUTO_RES), "Auto: best resolution for FPS", "AUTO"}, {0, NULL, NULL}, }; @@ -299,105 +305,65 @@ static GType gst_zedxonesrc_coord_sys_get_type(void) { } /* pad templates */ +// Use ranged caps to allow flexible output resolution negotiation. +// The actual resolution is determined by the camera and downstream negotiation. +// Non-NVMM formats (BGRA) support resize via sl::Resolution in retrieveImage(). +// NVMM formats (NV12 zero-copy) require fixed resolution (direct buffer mapping). static GstStaticPadTemplate gst_zedxonesrc_src_template = GST_STATIC_PAD_TEMPLATE( "src", GST_PAD_SRC, GST_PAD_ALWAYS, - GST_STATIC_CAPS(("video/x-raw, " // Color 4K - "format = (string)BGRA, " - "width = (int)3840, " - "height = (int)2160, " - "framerate = (fraction) { 15, 30 }" - ";" - "video/x-raw, " // Color QHDPLUS - "format = (string)BGRA, " - "width = (int)3200, " - "height = (int)1800, " - "framerate = (fraction) { 15, 30 }" - ";" - "video/x-raw, " // Color HD1200 - "format = (string)BGRA, " - "width = (int)1920, " - "height = (int)1200, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Color HD1080 - "format = (string)BGRA, " - "width = (int)1920, " - "height = (int)1080, " - "framerate = (fraction) { 15, 30, 60 }" - ";" - "video/x-raw, " // Color SVGA - "format = (string)BGRA, " - "width = (int)960, " - "height = (int)600, " + GST_STATIC_CAPS(("video/x-raw, " + "format = (string){BGRA, BGR, GRAY8}, " + "width = (int) [ 32, 3840 ], " + "height = (int) [ 32, 2160 ], " "framerate = (fraction) { 15, 30, 60, 120 }" #ifdef SL_ENABLE_ADVANCED_CAPTURE_API ";" - "video/x-raw(memory:NVMM), " // NV12 4K (zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy SVGA "format = (string)NV12, " - "width = (int)3840, " - "height = (int)2160, " - "framerate = (fraction) { 15, 30 }" + "width = (int)960, " + "height = (int)600, " + "framerate = (fraction) { 15, 30, 60, 120 }" ";" - "video/x-raw(memory:NVMM), " // NV12 QHDPLUS (zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy HD1080 "format = (string)NV12, " - "width = (int)3200, " - "height = (int)1800, " - "framerate = (fraction) { 15, 30 }" + "width = (int)1920, " + "height = (int)1080, " + "framerate = (fraction) { 15, 30, 60 }" ";" - "video/x-raw(memory:NVMM), " // NV12 HD1200 (zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy HD1200 "format = (string)NV12, " "width = (int)1920, " "height = (int)1200, " "framerate = (fraction) { 15, 30, 60 }" ";" - "video/x-raw(memory:NVMM), " // NV12 HD1080 (zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy QHDPLUS "format = (string)NV12, " - "width = (int)1920, " - "height = (int)1080, " - "framerate = (fraction) { 15, 30, 60 }" + "width = (int)3200, " + "height = (int)1800, " + "framerate = (fraction) { 15, 30 }" ";" - "video/x-raw(memory:NVMM), " // NV12 SVGA (zero-copy) + "video/x-raw(memory:NVMM), " // NV12 zero-copy 4K "format = (string)NV12, " - "width = (int)960, " - "height = (int)600, " - "framerate = (fraction) { 15, 30, 60, 120 }" + "width = (int)3840, " + "height = (int)2160, " + "framerate = (fraction) { 15, 30 }" #endif ))); /* Tools */ -bool resol_to_w_h(const GstZedXOneSrcRes &resol, guint32 &out_w, guint32 &out_h) { - switch (resol) { - case GST_ZEDXONESRC_SVGA: - out_w = 960; - out_h = 600; - break; - - case GST_ZEDXONESRC_1080P: - out_w = 1920; - out_h = 1080; - break; - - case GST_ZEDXONESRC_1200P: - out_w = 1920; - out_h = 1200; - break; - - case GST_ZEDXONESRC_QHDPLUS: - out_w = 3200; - out_h = 1800; - break; +/* Forward declaration — defined after the mode table */ +static sl::RESOLUTION gst_zedxonesrc_get_sl_resolution(GstZedXOneSrcRes resol); - case GST_ZEDXONESRC_4K: - out_w = 3840; - out_h = 2160; - break; - - default: +bool resol_to_w_h(const GstZedXOneSrcRes &resol, guint32 &out_w, guint32 &out_h) { + sl::RESOLUTION sl_res = gst_zedxonesrc_get_sl_resolution(resol); + if (sl_res == sl::RESOLUTION::LAST) { out_w = -1; out_h = -1; return false; } - + sl::Resolution native = sl::getResolution(sl_res); + out_w = native.width; + out_h = native.height; return true; } @@ -426,6 +392,7 @@ static void gst_zedxonesrc_class_init(GstZedXOneSrcClass *klass) { gstbasesrc_class->stop = GST_DEBUG_FUNCPTR(gst_zedxonesrc_stop); gstbasesrc_class->get_caps = GST_DEBUG_FUNCPTR(gst_zedxonesrc_get_caps); gstbasesrc_class->set_caps = GST_DEBUG_FUNCPTR(gst_zedxonesrc_set_caps); + gstbasesrc_class->fixate = GST_DEBUG_FUNCPTR(gst_zedxonesrc_fixate); gstbasesrc_class->unlock = GST_DEBUG_FUNCPTR(gst_zedxonesrc_unlock); gstbasesrc_class->unlock_stop = GST_DEBUG_FUNCPTR(gst_zedxonesrc_unlock_stop); gstbasesrc_class->query = GST_DEBUG_FUNCPTR(gst_zedxonesrc_query); @@ -705,6 +672,8 @@ static void gst_zedxonesrc_reset(GstZedXOneSrc *src) { src->_outFramesize = 0; src->_isStarted = FALSE; + src->_stopRequested = FALSE; + src->_resolvedStreamType = -1; // Reset so AUTO re-negotiates on next start src->_bufferIndex = 0; if (src->_caps) { @@ -1070,6 +1039,13 @@ void gst_zedxonesrc_finalize(GObject *object) { GST_TRACE_OBJECT(src, "gst_zedxonesrc_finalize"); + /* Ensure camera is closed even if stop() was never called + * (e.g. abnormal shutdown, signal interruption) */ + if (src->_zed->isOpened()) { + GST_WARNING_OBJECT(src, "Camera still open in finalize — forcing close"); + src->_zed->close(); + } + /* clean up object here */ if (src->_caps) { gst_caps_unref(src->_caps); @@ -1166,6 +1142,116 @@ static void gst_zedxonesrc_resolve_stream_type(GstZedXOneSrc *src) { src->_resolvedStreamType); } +/* Camera mode: maps GstZedXOneSrcRes enum to sl::RESOLUTION. + * Resolution dimensions come from sl::getResolution(). + * FPS validity is queried dynamically via sl::isAvailable(fps, resolution, model). */ +typedef struct { + GstZedXOneSrcRes resolution; + sl::RESOLUTION sl_resolution; +} ZedXOneCameraMode; + +/* Camera modes ordered by quality (highest first) for AUTO resolution selection */ +static const ZedXOneCameraMode xone_camera_modes[] = { + {GST_ZEDXONESRC_4K, sl::RESOLUTION::HD4K}, + {GST_ZEDXONESRC_QHDPLUS, sl::RESOLUTION::QHDPLUS}, + {GST_ZEDXONESRC_1200P, sl::RESOLUTION::HD1200}, + {GST_ZEDXONESRC_1080P, sl::RESOLUTION::HD1080}, + {GST_ZEDXONESRC_SVGA, sl::RESOLUTION::SVGA}, +}; +static const guint xone_camera_modes_count = G_N_ELEMENTS(xone_camera_modes); + +static const ZedXOneCameraMode *gst_zedxonesrc_get_camera_mode(GstZedXOneSrcRes resolution) { + for (guint i = 0; i < xone_camera_modes_count; ++i) { + if (xone_camera_modes[i].resolution == resolution) { + return &xone_camera_modes[i]; + } + } + return NULL; +} + +/* Get sl::RESOLUTION for a GstZedXOneSrcRes enum value */ +static sl::RESOLUTION gst_zedxonesrc_get_sl_resolution(GstZedXOneSrcRes resol) { + const ZedXOneCameraMode *mode = gst_zedxonesrc_get_camera_mode(resol); + return mode ? mode->sl_resolution : sl::RESOLUTION::LAST; +} + +/* + * Detect the camera model using sl::CameraOne::getDeviceList() (lightweight, no open needed). + * If camera_id or camera_sn is set, match the specific device; otherwise find + * the first genuine mono camera using sl::isCameraOne(). + */ +static sl::MODEL gst_zedxonesrc_detect_camera_model(GstZedXOneSrc *src) { + auto devices = sl::CameraOne::getDeviceList(); + if (devices.empty()) { + GST_ERROR_OBJECT(src, "No ZED X One cameras detected"); + return sl::MODEL::LAST; + } + + // CameraOne::getDeviceList() may return stereo ZED X cameras on + // multi-camera rigs (known SDK bug). Always validate with isCameraOne(). + + // If a specific camera was requested, match it — but verify it is mono. + if (src->_cameraId != -1) { + for (const auto &dev : devices) { + if (dev.id == static_cast(src->_cameraId)) { + if (!sl::isCameraOne(dev.camera_model)) { + GST_WARNING("camera-id %d matched %s which is not a mono camera, ignoring", + src->_cameraId, sl::toString(dev.camera_model).c_str()); + break; // fall through to auto-detect below + } + GST_INFO("Detected camera model: %s (camera-id %d)", + sl::toString(dev.camera_model).c_str(), src->_cameraId); + return dev.camera_model; + } + } + } else if (src->_cameraSN != 0) { + for (const auto &dev : devices) { + if (dev.serial_number == static_cast(src->_cameraSN)) { + if (!sl::isCameraOne(dev.camera_model)) { + GST_WARNING("camera-sn %u matched %s which is not a mono camera, ignoring", + static_cast(src->_cameraSN), + sl::toString(dev.camera_model).c_str()); + break; // fall through to auto-detect below + } + GST_INFO("Detected camera model: %s (S/N %u)", + sl::toString(dev.camera_model).c_str(), + static_cast(src->_cameraSN)); + return dev.camera_model; + } + } + } + + // Find the first genuine mono camera in the list. + for (const auto &dev : devices) { + if (sl::isCameraOne(dev.camera_model)) { + GST_INFO("Detected camera model: %s (auto, device %d)", + sl::toString(dev.camera_model).c_str(), dev.id); + return dev.camera_model; + } + } + + // No mono camera at all — do NOT blindly use a stereo model. + GST_ERROR_OBJECT(src, "No mono (ZED X One) camera found among %zu device(s)", + devices.size()); + return sl::MODEL::LAST; +} + +/* + * Query the SDK for valid FPS values for a given resolution + camera model. + * Returns the count of valid FPS values written to out_fps (up to max_count). + */ +static guint gst_zedxonesrc_get_valid_fps(sl::RESOLUTION sl_res, sl::MODEL model, gint *out_fps, + guint max_count) { + const auto &candidate_fps = sl::getAvailableCameraFPS(); + guint count = 0; + for (size_t i = 0; i < candidate_fps.size() && count < max_count; ++i) { + if (sl::isAvailable(candidate_fps[i], sl_res, model)) { + out_fps[count++] = candidate_fps[i]; + } + } + return count; +} + static gboolean gst_zedxonesrc_calculate_caps(GstZedXOneSrc *src) { GST_TRACE_OBJECT(src, "gst_zedxonesrc_calculate_caps"); @@ -1173,32 +1259,125 @@ static gboolean gst_zedxonesrc_calculate_caps(GstZedXOneSrc *src) { gint stream_type = src->_resolvedStreamType; guint32 width, height; - gint fps; GstVideoInfo vinfo; - GstVideoFormat format = GST_VIDEO_FORMAT_BGRA; + GstVideoFormat format = GST_VIDEO_FORMAT_UNKNOWN; + const gchar *format_str = NULL; + gboolean use_ranged_caps = TRUE; + gboolean multi_format = FALSE; #ifdef SL_ENABLE_ADVANCED_CAPTURE_API if (stream_type == GST_ZEDXONESRC_RAW_NV12) { format = GST_VIDEO_FORMAT_NV12; - } + format_str = "NV12"; + use_ranged_caps = FALSE; // Zero-copy cannot resize + } else #endif + { + // Image stream type supports BGRA, BGR, GRAY8 via SDK VIEW variants + format = GST_VIDEO_FORMAT_BGRA; + format_str = "BGRA"; + multi_format = TRUE; + } if (!resol_to_w_h(static_cast(src->_cameraResolution), width, height)) { return FALSE; } - fps = src->_cameraFps; + // Store native camera resolution + src->_cameraWidth = width; + src->_cameraHeight = height; + + // Get the mode for the resolved camera resolution + const ZedXOneCameraMode *mode = + gst_zedxonesrc_get_camera_mode(static_cast(src->_cameraResolution)); + gint fps = src->_cameraFps; // Already clamped in start() + + // Query valid FPS from SDK for this resolution + camera model + auto cam_info = src->_zed->getCameraInformation(); + sl::MODEL caps_model = cam_info.camera_model; + gint valid_fps[8]; + guint valid_fps_count = 0; + if (mode) { + valid_fps_count = + gst_zedxonesrc_get_valid_fps(mode->sl_resolution, caps_model, valid_fps, 8); + } + + if (src->_caps) { + gst_caps_unref(src->_caps); + src->_caps = NULL; + } if (format != GST_VIDEO_FORMAT_UNKNOWN) { + if (use_ranged_caps && width >= GST_ZEDXONESRC_MIN_OUTPUT_SIZE && + height >= GST_ZEDXONESRC_MIN_OUTPUT_SIZE) { + // Create caps with ranged width/height for flexible output resolution. + // Downstream can negotiate any size from [32, max] and we'll use + // sl::Resolution in retrieveImage() to resize. + src->_caps = gst_caps_new_empty_simple("video/x-raw"); + GstStructure *s = gst_caps_get_structure(src->_caps, 0); + + // Set format: multi-format list (BGRA preferred) or single format + if (multi_format) { + GValue fmt_list = G_VALUE_INIT; + g_value_init(&fmt_list, GST_TYPE_LIST); + const gchar *formats[] = {"BGRA", "BGR", "GRAY8"}; + for (guint i = 0; i < G_N_ELEMENTS(formats); ++i) { + GValue v = G_VALUE_INIT; + g_value_init(&v, G_TYPE_STRING); + g_value_set_string(&v, formats[i]); + gst_value_list_append_value(&fmt_list, &v); + g_value_unset(&v); + } + gst_structure_set_value(s, "format", &fmt_list); + g_value_unset(&fmt_list); + } else { + gst_structure_set(s, "format", G_TYPE_STRING, format_str, NULL); + } + + // Set ranged width/height + gst_structure_set(s, "width", GST_TYPE_INT_RANGE, GST_ZEDXONESRC_MIN_OUTPUT_SIZE, + (gint) width, "height", GST_TYPE_INT_RANGE, + GST_ZEDXONESRC_MIN_OUTPUT_SIZE, (gint) height, NULL); + + // Set framerate as a list of valid values for this camera resolution + if (valid_fps_count > 1) { + GValue fps_list = G_VALUE_INIT; + g_value_init(&fps_list, GST_TYPE_LIST); + for (guint i = 0; i < valid_fps_count; ++i) { + GValue fps_val = G_VALUE_INIT; + g_value_init(&fps_val, GST_TYPE_FRACTION); + gst_value_set_fraction(&fps_val, valid_fps[i], 1); + gst_value_list_append_value(&fps_list, &fps_val); + g_value_unset(&fps_val); + } + gst_structure_set_value(s, "framerate", &fps_list); + g_value_unset(&fps_list); + } else { + gst_structure_set(s, "framerate", GST_TYPE_FRACTION, fps, 1, NULL); + } + + GST_INFO_OBJECT(src, "Created ranged caps: format=%s%s, width=[%d,%d], height=[%d,%d]", + format_str, multi_format ? " (+BGR, GRAY8)" : "", + GST_ZEDXONESRC_MIN_OUTPUT_SIZE, width, GST_ZEDXONESRC_MIN_OUTPUT_SIZE, + height); + } else { + // Fixed caps for zero-copy modes or when ranged caps not applicable + gst_video_info_init(&vinfo); + gst_video_info_set_format(&vinfo, format, width, height); + vinfo.fps_n = fps; + vinfo.fps_d = 1; + src->_caps = gst_video_info_to_caps(&vinfo); + } + + // Calculate frame size based on maximum resolution gst_video_info_init(&vinfo); gst_video_info_set_format(&vinfo, format, width, height); - if (src->_caps) { - gst_caps_unref(src->_caps); - } src->_outFramesize = (guint) GST_VIDEO_INFO_SIZE(&vinfo); - vinfo.fps_n = fps; - vinfo.fps_d = 1; - src->_caps = gst_video_info_to_caps(&vinfo); + + // Initialize output resolution and format (will be updated in set_caps) + src->_outputWidth = width; + src->_outputHeight = height; + src->_outputFormat = format; #ifdef SL_ENABLE_ADVANCED_CAPTURE_API // Add memory:NVMM feature for zero-copy NV12 mode @@ -1211,12 +1390,112 @@ static gboolean gst_zedxonesrc_calculate_caps(GstZedXOneSrc *src) { } gst_base_src_set_blocksize(GST_BASE_SRC(src), src->_outFramesize); - gst_base_src_set_caps(GST_BASE_SRC(src), src->_caps); + + // Only call gst_base_src_set_caps for fixed caps (non-ranged) + // For ranged caps, negotiation happens via get_caps/set_caps + if (!use_ranged_caps) { + gst_base_src_set_caps(GST_BASE_SRC(src), src->_caps); + } GST_DEBUG_OBJECT(src, "Created caps %" GST_PTR_FORMAT, src->_caps); return TRUE; } +/* Return the closest valid FPS for the current camera resolution + detected model */ +static gint gst_zedxonesrc_select_fps(GstZedXOneSrc *src, gint requested_fps) { + if (requested_fps <= 0) { + requested_fps = DEFAULT_PROP_CAM_FPS; + } + + const ZedXOneCameraMode *mode = + gst_zedxonesrc_get_camera_mode(static_cast(src->_cameraResolution)); + if (!mode) { + return requested_fps; + } + + sl::MODEL camera_model = gst_zedxonesrc_detect_camera_model(src); + gint valid_fps[8]; + guint count = gst_zedxonesrc_get_valid_fps(mode->sl_resolution, camera_model, valid_fps, 8); + if (count == 0) { + return requested_fps; + } + + gint best = valid_fps[0]; + gint best_diff = ABS(requested_fps - best); + for (guint i = 1; i < count; ++i) { + gint diff = ABS(requested_fps - valid_fps[i]); + if (diff < best_diff) { + best = valid_fps[i]; + best_diff = diff; + } + } + + return best; +} + +/* + * Resolve camera resolution when set to AUTO. + * + * Strategy: detect the camera model using sl::CameraOne::getDeviceList() + * and use sl::isAvailable(resolution, model) to filter compatible modes. + * Then pick the highest-quality resolution that supports the requested FPS. + * + * If the requested FPS doesn't exactly match any mode, the nearest valid + * FPS is selected first, then the best resolution for that FPS is chosen. + */ +static void gst_zedxonesrc_resolve_auto_resolution(GstZedXOneSrc *src) { + gint requested_fps = src->_cameraFps; + if (requested_fps <= 0) { + requested_fps = DEFAULT_PROP_CAM_FPS; + } + + /* Detect camera model to filter incompatible resolutions */ + sl::MODEL camera_model = gst_zedxonesrc_detect_camera_model(src); + if (camera_model == sl::MODEL::LAST) { + GST_ERROR_OBJECT(src, "Cannot resolve AUTO resolution: no camera detected"); + return; + } + + /* First: find the closest valid FPS across compatible modes */ + const auto &candidate_fps = sl::getAvailableCameraFPS(); + gint best_fps = requested_fps; + gint best_fps_diff = G_MAXINT; + for (guint i = 0; i < xone_camera_modes_count; ++i) { + if (!sl::isAvailable(xone_camera_modes[i].sl_resolution, camera_model)) + continue; + for (size_t j = 0; j < candidate_fps.size(); ++j) { + if (!sl::isAvailable(candidate_fps[j], xone_camera_modes[i].sl_resolution, + camera_model)) + continue; + gint diff = ABS(requested_fps - candidate_fps[j]); + if (diff < best_fps_diff) { + best_fps_diff = diff; + best_fps = candidate_fps[j]; + } + } + } + + /* Second: pick the highest quality compatible resolution supporting that FPS */ + for (guint i = 0; i < xone_camera_modes_count; ++i) { + if (!sl::isAvailable(xone_camera_modes[i].sl_resolution, camera_model)) + continue; + if (sl::isAvailable(best_fps, xone_camera_modes[i].sl_resolution, camera_model)) { + src->_cameraResolution = xone_camera_modes[i].resolution; + src->_cameraFps = best_fps; + sl::Resolution native = sl::getResolution(xone_camera_modes[i].sl_resolution); + GST_INFO("AUTO resolution resolved: %dx%d @ %d FPS (requested %d FPS, model %s)", + (int) native.width, (int) native.height, best_fps, requested_fps, + sl::toString(camera_model).c_str()); + return; + } + } + + /* Fallback */ + src->_cameraResolution = GST_ZEDXONESRC_1200P; + src->_cameraFps = 30; + GST_WARNING("AUTO resolution fallback to 1200P@30"); +} + static gboolean gst_zedxonesrc_start(GstBaseSrc *bsrc) { GstZedXOneSrc *src = GST_ZED_X_ONE_SRC(bsrc); @@ -1227,7 +1506,16 @@ static gboolean gst_zedxonesrc_start(GstBaseSrc *bsrc) { #endif sl::ERROR_CODE ret; - GST_TRACE_OBJECT(src, "gst_zedxonesrc_calculate_caps"); + GST_TRACE_OBJECT(src, "gst_zedxonesrc_start"); + + // ----> Resolve AUTO camera resolution based on requested FPS + if (src->_cameraResolution == GST_ZEDXONESRC_AUTO_RES) { + gst_zedxonesrc_resolve_auto_resolution(src); + } else { + // Explicit resolution: clamp FPS to nearest valid for this resolution + src->_cameraFps = gst_zedxonesrc_select_fps(src, src->_cameraFps); + GST_INFO("Explicit resolution: clamped FPS to %d", src->_cameraFps); + } // ----> Set init parameters sl::InitParametersOne init_params; @@ -1248,25 +1536,41 @@ static gboolean gst_zedxonesrc_start(GstBaseSrc *bsrc) { } else if (src->_streamIp->len != 0) { init_params.input.setFromStream(sl::String(src->_streamIp->str), src->_streamPort); GST_INFO(" * Input Stream: %s:%d", src->_streamIp->str, src->_streamPort); + } else { + // No explicit input specified — auto-detect the first ZED X One camera. + // On multi-camera rigs (e.g. 4x ZED X + 2x ZED X One) + // CameraOne::getDeviceList() may return ALL GMSL cameras including + // stereo ZED X models. Filter using sl::isCameraOne() to find a + // genuine mono camera. + auto devices = sl::CameraOne::getDeviceList(); + bool found = false; + for (const auto &dev : devices) { + if (sl::isCameraOne(dev.camera_model)) { + init_params.input.setFromCameraID(dev.id); + GST_INFO(" * Auto-selected ZED X One camera ID: %d (S/N: %u, model: %s)", dev.id, + dev.serial_number, sl::toString(dev.camera_model).c_str()); + found = true; + break; + } + } + if (!found) { + if (!devices.empty()) { + GST_ELEMENT_ERROR(src, RESOURCE, NOT_FOUND, + ("No mono (ZED X One) camera found — %zu device(s) are all stereo", + devices.size()), (NULL)); + } else { + GST_ELEMENT_ERROR(src, RESOURCE, NOT_FOUND, + ("No ZED cameras detected"), (NULL)); + } + return FALSE; + } } - switch (src->_cameraResolution) { - case GST_ZEDXONESRC_SVGA: - init_params.camera_resolution = sl::RESOLUTION::SVGA; - break; - case GST_ZEDXONESRC_1080P: - init_params.camera_resolution = sl::RESOLUTION::HD1080; - break; - case GST_ZEDXONESRC_1200P: - init_params.camera_resolution = sl::RESOLUTION::HD1200; - break; - case GST_ZEDXONESRC_QHDPLUS: - init_params.camera_resolution = sl::RESOLUTION::QHDPLUS; - break; - case GST_ZEDXONESRC_4K: - init_params.camera_resolution = sl::RESOLUTION::HD4K; - break; - default: + const ZedXOneCameraMode *resolved_mode = + gst_zedxonesrc_get_camera_mode(static_cast(src->_cameraResolution)); + if (resolved_mode) { + init_params.camera_resolution = resolved_mode->sl_resolution; + } else { GST_ELEMENT_ERROR(src, RESOURCE, NOT_FOUND, ("Failed to set camera resolution"), (NULL)); return FALSE; } @@ -1551,6 +1855,39 @@ static gboolean gst_zedxonesrc_set_caps(GstBaseSrc *bsrc, GstCaps *caps) { goto unsupported_caps; } + // Capture the negotiated output resolution and format + src->_outputWidth = GST_VIDEO_INFO_WIDTH(&vinfo); + src->_outputHeight = GST_VIDEO_INFO_HEIGHT(&vinfo); + src->_outputFormat = GST_VIDEO_INFO_FORMAT(&vinfo); + + GST_INFO_OBJECT(src, "Negotiated format: %s", gst_video_format_to_string(src->_outputFormat)); + +#ifdef SL_ENABLE_ADVANCED_CAPTURE_API + // Validate NV12 zero-copy sizes: the negotiated resolution must match the camera + // native resolution exactly, since zero-copy maps the raw buffer directly. + if (src->_resolvedStreamType == GST_ZEDXONESRC_RAW_NV12) { + const gint width = src->_outputWidth; + const gint height = src->_outputHeight; + const guint cam_w = src->_cameraWidth; + const guint cam_h = src->_cameraHeight; + const gboolean valid_size = (width == (gint) cam_w && height == (gint) cam_h); + + if (!valid_size) { + GST_ERROR_OBJECT(src, "Invalid negotiated NV12 size %dx%d (expected camera size %ux%u)", + width, height, cam_w, cam_h); + return FALSE; + } + } +#endif + + // Recalculate frame size based on negotiated resolution + src->_outFramesize = (guint) GST_VIDEO_INFO_SIZE(&vinfo); + gst_base_src_set_blocksize(GST_BASE_SRC(src), src->_outFramesize); + + GST_INFO_OBJECT(src, "Negotiated output resolution: %dx%d (camera: %dx%d), framesize: %u", + src->_outputWidth, src->_outputHeight, src->_cameraWidth, src->_cameraHeight, + src->_outFramesize); + return TRUE; unsupported_caps: @@ -1558,6 +1895,44 @@ static gboolean gst_zedxonesrc_set_caps(GstBaseSrc *bsrc, GstCaps *caps) { return FALSE; } +static GstCaps *gst_zedxonesrc_fixate(GstBaseSrc *bsrc, GstCaps *caps) { + GstZedXOneSrc *src = GST_ZED_X_ONE_SRC(bsrc); + GstStructure *structure; + + GST_DEBUG_OBJECT(src, "Fixating caps %" GST_PTR_FORMAT, caps); + + caps = gst_caps_make_writable(caps); + structure = gst_caps_get_structure(caps, 0); + + // Default output to camera native resolution when downstream doesn't constrain. + // _outputWidth/_outputHeight are always initialized in start() before fixate is called. + gst_structure_fixate_field_nearest_int(structure, "width", src->_outputWidth); + gst_structure_fixate_field_nearest_int(structure, "height", src->_outputHeight); + + // Default framerate to the camera_fps (already clamped in start()). + // If downstream constrained the FPS list, pick the closest valid one. + { + gint fps_n = 0, fps_d = 1; + gint requested_fps = src->_cameraFps; + + if (gst_structure_get_fraction(structure, "framerate", &fps_n, &fps_d) && fps_d > 0) { + requested_fps = (gint) ((gdouble) fps_n / (gdouble) fps_d + 0.5); + } + + const gint selected_fps = gst_zedxonesrc_select_fps(src, requested_fps); + src->_cameraFps = selected_fps; + gst_structure_set(structure, "framerate", GST_TYPE_FRACTION, selected_fps, 1, NULL); + GST_INFO_OBJECT(src, "Fixated: %dx%d @ %d FPS (camera: %dx%d)", src->_outputWidth, + src->_outputHeight, selected_fps, src->_cameraWidth, src->_cameraHeight); + } + + // Chain up to parent fixate + caps = GST_BASE_SRC_CLASS(gst_zedxonesrc_parent_class)->fixate(bsrc, caps); + + GST_DEBUG_OBJECT(src, "Fixated caps %" GST_PTR_FORMAT, caps); + return caps; +} + static gboolean gst_zedxonesrc_unlock(GstBaseSrc *bsrc) { GstZedXOneSrc *src = GST_ZED_X_ONE_SRC(bsrc); @@ -1626,9 +2001,29 @@ static GstFlowReturn gst_zedxonesrc_create(GstPushSrc *psrc, GstBuffer **outbuf) // Use resolved stream type which accounts for AUTO negotiation gint stream_type = src->_resolvedStreamType; - // For non-NVMM modes, fall back to the default fill() path + // For non-NVMM modes, allocate a buffer and use the fill() path. + // NOTE: We cannot chain to GstPushSrcClass->create here because + // GstPushSrc does not set a default create vmethod in its class + // struct (it installs its dispatch at the GstBaseSrc level), so + // the parent create pointer is NULL. if (stream_type != GST_ZEDXONESRC_RAW_NV12) { - return GST_PUSH_SRC_CLASS(gst_zedxonesrc_parent_class)->create(psrc, outbuf); + GstFlowReturn ret; + GstBuffer *buf = NULL; + GstBaseSrc *bsrc = GST_BASE_SRC(psrc); + + ret = GST_BASE_SRC_CLASS(gst_zedxonesrc_parent_class) + ->alloc(bsrc, -1, gst_base_src_get_blocksize(bsrc), &buf); + if (ret != GST_FLOW_OK) + return ret; + + ret = gst_zedxonesrc_fill(psrc, buf); + if (ret != GST_FLOW_OK) { + gst_buffer_unref(buf); + return ret; + } + + *outbuf = buf; + return GST_FLOW_OK; } GST_TRACE_OBJECT(src, "gst_zedxonesrc_create (NVMM zero-copy)"); @@ -1768,11 +2163,14 @@ static GstFlowReturn gst_zedxonesrc_fill(GstPushSrc *psrc, GstBuffer *buf) { sl::ERROR_CODE ret; GstMapInfo minfo; GstClock *clock; - GstClockTime clock_time; + GstClockTime clock_time = GST_CLOCK_TIME_NONE; if (!src->_isStarted) { - src->_acqStartTime = gst_clock_get_time(gst_element_get_clock(GST_ELEMENT(src))); - + GstClock *start_clock = gst_element_get_clock(GST_ELEMENT(src)); + if (start_clock) { + src->_acqStartTime = gst_clock_get_time(start_clock); + gst_object_unref(start_clock); + } src->_isStarted = TRUE; } @@ -1780,7 +2178,10 @@ static GstFlowReturn gst_zedxonesrc_fill(GstPushSrc *psrc, GstBuffer *buf) { GST_TRACE(" Data Grabbing"); ret = src->_zed->grab(); - if (ret > sl::ERROR_CODE::SUCCESS) { + if (ret == sl::ERROR_CODE::END_OF_SVOFILE_REACHED) { + GST_INFO_OBJECT(src, "End of SVO file"); + return GST_FLOW_EOS; + } else if (ret != sl::ERROR_CODE::SUCCESS) { GST_ELEMENT_ERROR(src, RESOURCE, FAILED, ("Grabbing failed with error: '%s' - %s", sl::toString(ret).c_str(), sl::toVerbose(ret).c_str()), @@ -1792,8 +2193,10 @@ static GstFlowReturn gst_zedxonesrc_fill(GstPushSrc *psrc, GstBuffer *buf) { // ----> Clock update GST_TRACE("Clock update"); clock = gst_element_get_clock(GST_ELEMENT(src)); - clock_time = gst_clock_get_time(clock); - gst_object_unref(clock); + if (clock) { + clock_time = gst_clock_get_time(clock); + gst_object_unref(clock); + } // <---- Clock update // Memory mapping @@ -1819,16 +2222,41 @@ static GstFlowReturn gst_zedxonesrc_fill(GstPushSrc *psrc, GstBuffer *buf) { return true; }; - const sl::VIEW view_type = - src->_outputRectifiedImage ? sl::VIEW::LEFT : sl::VIEW::LEFT_UNRECTIFIED; - ret = src->_zed->retrieveImage(img, view_type, sl::MEM::CPU); + // Select sl::VIEW based on negotiated format and rectification setting. + // SDK VIEW variants: LEFT/LEFT_UNRECTIFIED (BGRA), _BGR (BGR), _GRAY (GRAY8) + sl::VIEW view_type; + const GstVideoFormat fmt = src->_outputFormat; + if (src->_outputRectifiedImage) { + view_type = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::LEFT_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::LEFT_GRAY + : sl::VIEW::LEFT; + } else { + view_type = (fmt == GST_VIDEO_FORMAT_BGR) ? sl::VIEW::LEFT_UNRECTIFIED_BGR + : (fmt == GST_VIDEO_FORMAT_GRAY8) ? sl::VIEW::LEFT_UNRECTIFIED_GRAY + : sl::VIEW::LEFT_UNRECTIFIED; + } + + // Use negotiated output resolution for retrieveImage if different from camera resolution + // Note: Pass sl::Resolution only if resizing is needed, to avoid potential SDK issues + if (src->_outputWidth != src->_cameraWidth || src->_outputHeight != src->_cameraHeight) { + sl::Resolution out_res(src->_outputWidth, src->_outputHeight); + ret = src->_zed->retrieveImage(img, view_type, sl::MEM::CPU, out_res); + } else { + ret = src->_zed->retrieveImage(img, view_type, sl::MEM::CPU); + } if (!check_ret(ret)) return GST_FLOW_ERROR; // <---- Retrieve images // Memory copy GST_TRACE("Memory copy"); - memcpy(minfo.data, img.getPtr(), minfo.size); + if (fmt == GST_VIDEO_FORMAT_BGR) { + memcpy(minfo.data, img.getPtr(), minfo.size); + } else if (fmt == GST_VIDEO_FORMAT_GRAY8) { + memcpy(minfo.data, img.getPtr(), minfo.size); + } else { + memcpy(minfo.data, img.getPtr(), minfo.size); + } // ----> Info metadata GST_TRACE("Info metadata"); diff --git a/gst-zedxone-src/gstzedxonesrc.h b/gst-zedxone-src/gstzedxonesrc.h index c38ca59..921a296 100644 --- a/gst-zedxone-src/gstzedxonesrc.h +++ b/gst-zedxone-src/gstzedxonesrc.h @@ -23,6 +23,7 @@ #define _GST_ZED_X_ONE_SRC_H_ #include +#include #include "sl/CameraOne.hpp" @@ -106,6 +107,14 @@ struct _GstZedXOneSrc { GstCaps *_caps; // Stream caps guint _outFramesize; // Output frame size in byte + + // Resolution and format tracking for flexible output caps + guint _cameraWidth; // Native camera resolution width + guint _cameraHeight; // Native camera resolution height + + guint _outputWidth; // Negotiated output resolution width + guint _outputHeight; // Negotiated output resolution height + GstVideoFormat _outputFormat; // Negotiated pixel format (BGRA, BGR, GRAY8...) }; struct _GstZedXOneSrcClass { diff --git a/scripts/jetson/common.sh b/scripts/jetson/common.sh index 73bd393..8bef56b 100755 --- a/scripts/jetson/common.sh +++ b/scripts/jetson/common.sh @@ -11,6 +11,24 @@ # - Pipeline building helpers that work for both camera types # ============================================================================= +# --- Auto-detect nvvidconv vs nvvideoconvert --- +# JP5 (L4T R35.x) has "nvvidconv", JP6+ (L4T R36.x) has "nvvideoconvert" +get_nvvidconv_element() { + if gst-inspect-1.0 nvvideoconvert > /dev/null 2>&1; then + echo "nvvideoconvert" + elif gst-inspect-1.0 nvvidconv > /dev/null 2>&1; then + echo "nvvidconv" + else + echo "" + fi +} +NVVIDCONV=$(get_nvvidconv_element) + +# --- Ensure DISPLAY is set for EGL-CUDA interop --- +if [ -z "$DISPLAY" ]; then + export DISPLAY=:0 +fi + # Check if zedsrc plugin supports NV12 zero-copy (stream-type=6) # This requires ZED SDK 5.2+ with Advanced Capture API compiled in zedsrc_supports_nv12() { @@ -21,6 +39,14 @@ zedsrc_supports_nv12() { return 1 } +# Check if zedxonesrc plugin supports NV12 zero-copy (stream-type=1) +zedxonesrc_supports_nv12() { + if gst-inspect-1.0 zedxonesrc 2>/dev/null | grep -q "NV12 zero-copy"; then + return 0 + fi + return 1 +} + # Detect if a GMSL camera (ZED X / ZED X Mini) is available # Returns 0 if GMSL camera is detected, 1 if USB camera or no camera detect_gmsl_camera() { @@ -154,7 +180,7 @@ build_nvmm_converter() { echo "" # GMSL already outputs NVMM NV12 else # USB cameras output BGRA on system memory, need to convert to NVMM NV12 - echo "! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12'" + echo "! $NVVIDCONV ! 'video/x-raw(memory:NVMM),format=NV12'" fi } @@ -170,7 +196,7 @@ build_encode_source() { echo "zedsrc stream-type=6 camera-resolution=$resolution camera-fps=$fps" else # USB: Need conversion to NVMM - echo "zedsrc stream-type=0 camera-resolution=$resolution camera-fps=$fps ! nvvideoconvert ! 'video/x-raw(memory:NVMM),format=NV12'" + echo "zedsrc stream-type=0 camera-resolution=$resolution camera-fps=$fps ! $NVVIDCONV ! 'video/x-raw(memory:NVMM),format=NV12'" fi } @@ -201,14 +227,19 @@ check_gst_plugin() { return 0 } +# Check if nvvidconv/nvvideoconvert is available +has_nvvidconv() { + [ -n "$NVVIDCONV" ] +} + # Check if hardware H.265 encoder is available # Orin Nano only has NVDEC (decoder), no NVENC (encoder) has_hw_h265_encoder() { - if gst-inspect-1.0 nvv4l2h265enc > /dev/null 2>&1; then + if gst-inspect-1.0 nvv4l2h265enc > /dev/null 2>&1 && [ -n "$NVVIDCONV" ]; then # Plugin exists, but check if it actually works (Orin Nano has the plugin but no HW) - # Try a quick encode test + # Try a quick encode test using the correct nvvidconv element for this platform if timeout 2 gst-launch-1.0 videotestsrc num-buffers=1 ! \ - "video/x-raw,width=320,height=240" ! nvvideoconvert ! \ + "video/x-raw,width=320,height=240" ! $NVVIDCONV ! \ "video/x-raw(memory:NVMM),format=NV12" ! nvv4l2h265enc ! \ fakesink > /dev/null 2>&1; then return 0 @@ -219,9 +250,9 @@ has_hw_h265_encoder() { # Check if hardware H.264 encoder is available has_hw_h264_encoder() { - if gst-inspect-1.0 nvv4l2h264enc > /dev/null 2>&1; then + if gst-inspect-1.0 nvv4l2h264enc > /dev/null 2>&1 && [ -n "$NVVIDCONV" ]; then if timeout 2 gst-launch-1.0 videotestsrc num-buffers=1 ! \ - "video/x-raw,width=320,height=240" ! nvvideoconvert ! \ + "video/x-raw,width=320,height=240" ! $NVVIDCONV ! \ "video/x-raw(memory:NVMM),format=NV12" ! nvv4l2h264enc ! \ fakesink > /dev/null 2>&1; then return 0 @@ -264,8 +295,8 @@ check_encoding_plugins() { missing=1 fi - if ! check_gst_plugin nvvideoconvert; then - echo " NVIDIA GStreamer plugins may not be installed." + if ! has_nvvidconv; then + echo " NVIDIA GStreamer plugins (nvvidconv/nvvideoconvert) not found." missing=1 fi diff --git a/scripts/jetson/hw_encode_nv12.sh b/scripts/jetson/hw_encode_nv12.sh index d0baac9..fd6d101 100755 --- a/scripts/jetson/hw_encode_nv12.sh +++ b/scripts/jetson/hw_encode_nv12.sh @@ -8,7 +8,7 @@ # - Uses zero-copy NV12 for maximum performance # # For USB cameras or older SDK: -# - Uses BGRA with nvvideoconvert to NVMM +# - Uses BGRA with $NVVIDCONV to NVMM # # For Orin Nano (no NVENC): # - Falls back to software encoding (x265enc/x264enc) @@ -103,7 +103,7 @@ run_pipeline() { nv3dsink sync=false else gst-launch-1.0 zedsrc camera-resolution=$RES_PROP camera-fps=$FPS stream-type=0 ! \ - nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false + $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false fi return fi @@ -131,7 +131,7 @@ run_pipeline() { if [ -n "$DISPLAY" ] && xset q &>/dev/null 2>&1; then $duration_opt gst-launch-1.0 -e \ zedsrc camera-resolution=$RES_PROP camera-fps=$FPS stream-type=0 ! \ - nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ tee name=t \ t. ! queue ! nvv4l2h265enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h265parse ! mp4mux ! filesink location=$OUTPUT_FILE \ @@ -139,7 +139,7 @@ run_pipeline() { else $duration_opt gst-launch-1.0 -e \ zedsrc camera-resolution=$RES_PROP camera-fps=$FPS stream-type=0 ! \ - nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ nvv4l2h265enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h265parse ! mp4mux ! filesink location=$OUTPUT_FILE fi @@ -154,7 +154,7 @@ run_pipeline() { else $duration_opt gst-launch-1.0 -e \ zedsrc camera-resolution=$RES_PROP camera-fps=$FPS stream-type=0 ! \ - nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ nvv4l2h264enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h264parse ! mp4mux ! filesink location=$OUTPUT_FILE fi diff --git a/scripts/jetson/hw_encode_stereo.sh b/scripts/jetson/hw_encode_stereo.sh index 6e2d82c..9e17791 100755 --- a/scripts/jetson/hw_encode_stereo.sh +++ b/scripts/jetson/hw_encode_stereo.sh @@ -9,7 +9,7 @@ # - Uses zero-copy NV12 stereo for maximum performance (stream-type=7) # # For USB cameras or older SDK: -# - Uses BGRA left+right demuxed with nvvideoconvert to NVMM NV12 +# - Uses BGRA left+right demuxed with $NVVIDCONV to NVMM NV12 # # For Orin Nano (no NVENC): # - Falls back to software encoding (x265enc/x264enc) @@ -71,10 +71,10 @@ run_pipeline() { gst-launch-1.0 -e \ zedsrc camera-resolution=2 camera-fps=$FPS stream-type=2 ! \ zeddemux stream-data=false name=demux \ - demux.src_left ! queue ! nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + demux.src_left ! queue ! $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ nvv4l2h265enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h265parse ! mp4mux name=mux ! filesink location=$OUTPUT_FILE \ - demux.src_aux ! queue ! nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + demux.src_aux ! queue ! $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ nvv4l2h265enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h265parse ! mux. fi @@ -90,10 +90,10 @@ run_pipeline() { gst-launch-1.0 -e \ zedsrc camera-resolution=2 camera-fps=$FPS stream-type=2 ! \ zeddemux stream-data=false name=demux \ - demux.src_left ! queue ! nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + demux.src_left ! queue ! $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ nvv4l2h264enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h264parse ! mp4mux name=mux ! filesink location=$OUTPUT_FILE \ - demux.src_aux ! queue ! nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! \ + demux.src_aux ! queue ! $NVVIDCONV ! "video/x-raw(memory:NVMM),format=NV12" ! \ nvv4l2h264enc bitrate=$BITRATE preset-level=1 maxperf-enable=true ! \ h264parse ! mux. fi diff --git a/scripts/jetson/jetson-simple-view.sh b/scripts/jetson/jetson-simple-view.sh index 4bf5ff1..918013c 100755 --- a/scripts/jetson/jetson-simple-view.sh +++ b/scripts/jetson/jetson-simple-view.sh @@ -20,6 +20,6 @@ if is_zero_copy_available; then gst-launch-1.0 zedsrc stream-type=6 ! queue ! nv3dsink sync=false else # BGRA path with conversion to NVMM for display - gst-launch-1.0 zedsrc stream-type=0 ! queue ! nvvideoconvert ! \ + gst-launch-1.0 zedsrc stream-type=0 ! queue ! $NVVIDCONV ! \ "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false fi \ No newline at end of file diff --git a/scripts/jetson/local-rgb-depth-sens-csv.sh b/scripts/jetson/local-rgb-depth-sens-csv.sh index 9ce819b..396ed89 100755 --- a/scripts/jetson/local-rgb-depth-sens-csv.sh +++ b/scripts/jetson/local-rgb-depth-sens-csv.sh @@ -11,6 +11,6 @@ gst-launch-1.0 \ zedsrc stream-type=4 depth-mode=3 ! \ zeddemux stream-data=TRUE name=demux \ -demux.src_left ! queue ! autovideoconvert ! fpsdisplaysink \ -demux.src_aux ! queue ! autovideoconvert ! fpsdisplaysink \ +demux.src_left ! queue ! nvvideoconvert ! nv3dsink sync=false \ +demux.src_aux ! queue ! nvvideoconvert ! nv3dsink sync=false \ demux.src_data ! queue ! zeddatacsvsink location="${HOME}/test_csv.csv" append=FALSE diff --git a/scripts/jetson/local-rgb-od_multi-overlay.sh b/scripts/jetson/local-rgb-od_multi-overlay.sh index 38b80b8..02da0f5 100755 --- a/scripts/jetson/local-rgb-od_multi-overlay.sh +++ b/scripts/jetson/local-rgb-od_multi-overlay.sh @@ -11,4 +11,4 @@ gst-launch-1.0 \ zedsrc stream-type=0 od-enabled=true ! queue ! \ zedodoverlay ! queue ! \ -autovideoconvert ! fpsdisplaysink +nvvideoconvert ! nv3dsink sync=false diff --git a/scripts/jetson/local-rgb-rescale-od-overlay.sh b/scripts/jetson/local-rgb-rescale-od-overlay.sh index c0a6f01..7a84ea3 100755 --- a/scripts/jetson/local-rgb-rescale-od-overlay.sh +++ b/scripts/jetson/local-rgb-rescale-od-overlay.sh @@ -18,8 +18,8 @@ gst-launch-1.0 \ zeddatamux name=mux \ zedsrc stream-type=4 od-detection-model=0 od-enabled=true ! \ zeddemux stream-data=true is-depth=true name=demux \ -demux.src_aux ! queue ! autovideoconvert ! videoscale ! video/x-raw,width=672,height=376 ! queue ! fpsdisplaysink \ +demux.src_aux ! queue ! nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12,width=672,height=376" ! nv3dsink sync=false \ demux.src_data ! mux.sink_data \ -demux.src_left ! queue ! videoscale ! video/x-raw,width=672,height=376 ! mux.sink_video \ +demux.src_left ! queue ! nvvideoconvert ! video/x-raw,format=BGRA,width=672,height=376 ! mux.sink_video \ mux.src ! queue ! zedodoverlay ! queue ! \ -autovideoconvert ! fpsdisplaysink +nvvideoconvert ! nv3dsink sync=false diff --git a/scripts/jetson/local-rgb-skel_accurate-overlay.sh b/scripts/jetson/local-rgb-skel_accurate-overlay.sh index 75c2110..564bb3d 100755 --- a/scripts/jetson/local-rgb-skel_accurate-overlay.sh +++ b/scripts/jetson/local-rgb-skel_accurate-overlay.sh @@ -10,4 +10,4 @@ gst-launch-1.0 \ zedsrc stream-type=0 bt-enabled=true bt-detection-model=2 bt-format=2 ! queue ! \ zedodoverlay ! queue ! \ -autovideoconvert ! fpsdisplaysink +nvvideoconvert ! nv3dsink sync=false diff --git a/scripts/jetson/local-rgb-skel_fast-overlay.sh b/scripts/jetson/local-rgb-skel_fast-overlay.sh index c147577..41f8ef8 100755 --- a/scripts/jetson/local-rgb-skel_fast-overlay.sh +++ b/scripts/jetson/local-rgb-skel_fast-overlay.sh @@ -11,4 +11,4 @@ gst-launch-1.0 \ zedsrc stream-type=0 bt-enabled=true bt-detection-model=0 ! queue ! \ zedodoverlay ! queue ! \ -autovideoconvert ! fpsdisplaysink +nvvideoconvert ! nv3dsink sync=false diff --git a/scripts/jetson/local-rgb_left_depth-fps_rendering.sh b/scripts/jetson/local-rgb_left_depth-fps_rendering.sh index 4b81a47..3e65725 100755 --- a/scripts/jetson/local-rgb_left_depth-fps_rendering.sh +++ b/scripts/jetson/local-rgb_left_depth-fps_rendering.sh @@ -10,5 +10,5 @@ gst-launch-1.0 \ zedsrc stream-type=4 depth-mode=3 ! queue ! \ zeddemux name=demux \ -demux.src_left ! queue ! autovideoconvert ! fpsdisplaysink \ -demux.src_aux ! queue ! autovideoconvert ! fpsdisplaysink +demux.src_left ! queue ! nvvideoconvert ! nv3dsink sync=false \ +demux.src_aux ! queue ! nvvideoconvert ! nv3dsink sync=false diff --git a/scripts/jetson/local-rgb_left_right-fps_rendering.sh b/scripts/jetson/local-rgb_left_right-fps_rendering.sh index 782ec7b..433611f 100755 --- a/scripts/jetson/local-rgb_left_right-fps_rendering.sh +++ b/scripts/jetson/local-rgb_left_right-fps_rendering.sh @@ -11,5 +11,5 @@ gst-launch-1.0 \ zedsrc stream-type=2 ! queue ! \ zeddemux is-depth=false name=demux \ -demux.src_left ! queue ! autovideoconvert ! fpsdisplaysink \ -demux.src_aux ! queue ! autovideoconvert ! fpsdisplaysink +demux.src_left ! queue ! nvvideoconvert ! nv3dsink sync=false \ +demux.src_aux ! queue ! nvvideoconvert ! nv3dsink sync=false diff --git a/scripts/jetson/simple-depth-fps_rendering.sh b/scripts/jetson/simple-depth-fps_rendering.sh index ad3b7fb..88296fb 100755 --- a/scripts/jetson/simple-depth-fps_rendering.sh +++ b/scripts/jetson/simple-depth-fps_rendering.sh @@ -1,5 +1,8 @@ #!/bin/bash -e -# Example pipeline to acquire a NEURAL depth stream and render it displaying the current FPS using default values for each parameter +# Example pipeline to acquire a NEURAL depth stream and render it using +# hardware-accelerated display (nv3dsink) on Jetson. +# Depth stream is rendered as 16-bit greyscale, converted via GPU. -gst-launch-1.0 zedsrc stream-type=3 depth-mode=4 ! queue ! autovideoconvert ! queue ! fpsdisplaysink +gst-launch-1.0 zedsrc stream-type=3 depth-mode=4 ! queue ! \ + nvvideoconvert ! "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false diff --git a/scripts/jetson/simple-fps_rendering.sh b/scripts/jetson/simple-fps_rendering.sh index e6aa38a..7778a29 100755 --- a/scripts/jetson/simple-fps_rendering.sh +++ b/scripts/jetson/simple-fps_rendering.sh @@ -1,5 +1,17 @@ #!/bin/bash -e -# Example pipeline to acquire a stream and render it displaying the current FPS using default values for each parameter +# Example pipeline to acquire a ZED stream and render it using +# hardware-accelerated display (nv3dsink) on Jetson. +# Uses NV12 zero-copy for GMSL cameras (ZED X / ZED X Mini) when SDK 5.2+ +# is available, otherwise falls back to BGRA with GPU conversion. -gst-launch-1.0 zedsrc ! queue ! autovideoconvert ! queue ! fpsdisplaysink +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +if is_zero_copy_available; then + gst-launch-1.0 zedsrc stream-type=6 ! queue ! nv3dsink sync=false +else + gst-launch-1.0 zedsrc stream-type=0 ! queue ! nvvideoconvert ! \ + "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false +fi diff --git a/scripts/jetson/udp_stream.sh b/scripts/jetson/udp_stream.sh index 00e3b8d..4fb6c52 100755 --- a/scripts/jetson/udp_stream.sh +++ b/scripts/jetson/udp_stream.sh @@ -9,7 +9,7 @@ # - Uses zero-copy NV12 for maximum performance # # For USB cameras or older SDK: -# - Uses BGRA with nvvideoconvert to NVMM NV12 +# - Uses BGRA with $NVVIDCONV to NVMM NV12 # # Usage: # ./udp_stream.sh [client_ip] [port] [fps] [resolution] [bitrate] @@ -95,7 +95,7 @@ echo "Starting UDP stream..." if is_zero_copy_available; then SOURCE="zedsrc stream-type=6 camera-resolution=$RESOLUTION camera-fps=$FPS" else - SOURCE="zedsrc stream-type=0 camera-resolution=$RESOLUTION camera-fps=$FPS ! nvvideoconvert ! video/x-raw(memory:NVMM),format=NV12" + SOURCE="zedsrc stream-type=0 camera-resolution=$RESOLUTION camera-fps=$FPS ! $NVVIDCONV ! video/x-raw(memory:NVMM),format=NV12" fi # Select encoder based on hardware availability diff --git a/scripts/jetson/zedxone-4k-simple-fps_rendering.sh b/scripts/jetson/zedxone-4k-simple-fps_rendering.sh index 1363784..2f7d50a 100755 --- a/scripts/jetson/zedxone-4k-simple-fps_rendering.sh +++ b/scripts/jetson/zedxone-4k-simple-fps_rendering.sh @@ -1,5 +1,16 @@ #!/bin/bash -e -# Example pipeline to acquire a ZED X One 4K stream at 15 FPS and render it displaying the current FPS using default values for each parameter +# Example pipeline to acquire a ZED X One 4K stream at 15 FPS and render it +# using hardware-accelerated display (nv3dsink) on Jetson. +# Uses NV12 zero-copy (stream-type=1) for optimal performance. -gst-launch-1.0 zedxonesrc camera-resolution=3 camera-fps=15 ! queue ! autovideoconvert ! queue ! fpsdisplaysink +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +if zedxonesrc_supports_nv12; then + gst-launch-1.0 zedxonesrc stream-type=1 camera-resolution=3 camera-fps=15 ! queue ! nv3dsink sync=false +else + gst-launch-1.0 zedxonesrc stream-type=0 camera-resolution=3 camera-fps=15 ! queue ! nvvideoconvert ! \ + "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false +fi diff --git a/scripts/jetson/zedxone-sens-csv.sh b/scripts/jetson/zedxone-sens-csv.sh index 51974a3..7280f02 100755 --- a/scripts/jetson/zedxone-sens-csv.sh +++ b/scripts/jetson/zedxone-sens-csv.sh @@ -10,5 +10,5 @@ gst-launch-1.0 \ zedxonesrc ! \ zeddemux is-mono=TRUE stream-data=TRUE name=demux \ -demux.src_mono ! queue ! autovideoconvert ! fpsdisplaysink \ +demux.src_mono ! queue ! nvvideoconvert ! nv3dsink sync=false \ demux.src_data ! queue ! zeddatacsvsink location="${HOME}/test_csv.csv" append=FALSE diff --git a/scripts/jetson/zedxone-simple-60-fps_rendering.sh b/scripts/jetson/zedxone-simple-60-fps_rendering.sh index 2fb416c..08d051c 100755 --- a/scripts/jetson/zedxone-simple-60-fps_rendering.sh +++ b/scripts/jetson/zedxone-simple-60-fps_rendering.sh @@ -1,5 +1,16 @@ #!/bin/bash -e -# Example pipeline to acquire a ZED X One 1200p stream at 60 FPS and render it displaying the current FPS using default values for each parameter +# Example pipeline to acquire a ZED X One 1200p stream at 60 FPS and render it +# using hardware-accelerated display (nv3dsink) on Jetson. +# Uses NV12 zero-copy (stream-type=1) for optimal performance. -gst-launch-1.0 zedxonesrc camera-resolution=2 camera-fps=60 ! queue ! autovideoconvert ! queue ! fpsdisplaysink +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +if zedxonesrc_supports_nv12; then + gst-launch-1.0 zedxonesrc stream-type=1 camera-resolution=2 camera-fps=60 ! queue ! nv3dsink sync=false +else + gst-launch-1.0 zedxonesrc stream-type=0 camera-resolution=2 camera-fps=60 ! queue ! nvvideoconvert ! \ + "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false +fi diff --git a/scripts/jetson/zedxone-simple-fps_rendering.sh b/scripts/jetson/zedxone-simple-fps_rendering.sh index bc6c519..0f415ac 100755 --- a/scripts/jetson/zedxone-simple-fps_rendering.sh +++ b/scripts/jetson/zedxone-simple-fps_rendering.sh @@ -1,5 +1,16 @@ #!/bin/bash -e -# Example pipeline to acquire a ZED X One stream and render it displaying the current FPS using default values for each parameter +# Example pipeline to acquire a ZED X One stream and render it using +# hardware-accelerated display (nv3dsink) on Jetson. +# Uses NV12 zero-copy (stream-type=1) for optimal performance. -gst-launch-1.0 zedxonesrc ! queue ! autovideoconvert ! queue ! fpsdisplaysink +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +if zedxonesrc_supports_nv12; then + gst-launch-1.0 zedxonesrc stream-type=1 ! queue ! nv3dsink sync=false +else + gst-launch-1.0 zedxonesrc stream-type=0 ! queue ! nvvideoconvert ! \ + "video/x-raw(memory:NVMM),format=NV12" ! nv3dsink sync=false +fi diff --git a/scripts/linux/rtsp-rgb-h264-sender.sh b/scripts/linux/rtsp-rgb-h264-sender.sh index 93ef648..a3cd7e4 100755 --- a/scripts/linux/rtsp-rgb-h264-sender.sh +++ b/scripts/linux/rtsp-rgb-h264-sender.sh @@ -11,7 +11,7 @@ export SERVER_IP=${HOST_IPS[0]} gst-zed-rtsp-launch -a ${SERVER_IP} \ - zedsrc stream-type=0 ! identity silent=false ! \ + zedsrc stream-type=0 ! \ videoconvert ! video/x-raw, format=I420 ! \ - x264enc tune=zerolatency bitrate=50000 speed-preset=ultrafast key-int-max=30 qp-min=8 qp-max=51 qp-step=1 ! \ + x264enc tune=zerolatency bitrate=8000 speed-preset=ultrafast key-int-max=30 qp-min=8 qp-max=51 qp-step=1 ! \ rtph264pay config-interval=-1 mtu=1500 pt=96 name=pay0