ZED X stream crashing with multiple cameras

I am experiencing an issue with cameras/streaming crashing with my camera system. Here is a (hopefully) detailed description of the system I have currently:

I have 3 cameras (2x ZED X Mini, 1x ZED X) connected to an Nvidia Jetson Orin AGX via StereoLabs’ capture card with GMSL2 fakra cables.

After streaming for some amount of time, one of the cameras will crash. The only configuration for which we’ve found to have no issues with is 1 camera, SVGA resolution at 60 fps. Here is the behavior for several cases:

  • 1x SVGA @ 60, any bitrate: completely stable
  • 1x 1080 @ 60, 32k bitrate: ~48fps, (do we crash?)
  • 2x SVGA @ 60, 12k bitrate: 60fps, ~50fps (respectively)
  • 3x SVGA @ 60, 12k bitrate: all cameras start below 60 fps, but over a long period of time (>20 minutes) creep to 60 fps each. After a few minutes, one or two of the cameras dips far below 60 fps (<45 fps) and then crashes
  • 3x SVGA @ 60, 32k bitrate: one camera sits decently comfortably at 60fps, while the two others sit around 30-40, and after some time the program crashes.
  • 1x 1080 @ 60, 2x SVGA @ 60, 32k bitrate: no camera hits 60fps, crashes fairly fast.

After doing some profiling, I’ve deduced that the issue is not CPU or GPU limitations. Reducing bitrate helps make the system last a lot longer but still eventually crashes.

I’ve attached our code below:

#!/usr/bin/env python3

import pyzed.sl as sl
import sys
import time
import argparse
import threading
import signal

# Global variable to handle exit
exit_app = False

def signal_handler(signal, frame):
    """Handle Ctrl+C to properly exit the program"""
    global exit_app
    exit_app = True
    print("\nCtrl+C pressed. Exiting...")

def parse_args():
    parser = argparse.ArgumentParser(description='ZED Multi-Camera Streaming Sender')
    parser.add_argument('--head_resolution', type=str, default='HD1080', 
                       choices=['HD1200', 'HD1080', 'SVGA', 'None'],
                       help='Camera resolution (default: HD1080)')
    parser.add_argument('--wrist_resolution', type=str, default='SVGA', 
                       choices=['HD1200', 'HD1080', 'SVGA', 'None'],
                       help='Camera resolution (default: SVGA)')
    parser.add_argument('--fps', type=int, default=60,
                       help='Frames per second (default: 60)')
    parser.add_argument('--base-port', type=int, default=30000,
                       help='Base streaming port - each camera will use base_port + (2 * camera_index) (default: 30000)')
    parser.add_argument('--bitrate', type=int, default=32768,
                       help='Streaming bitrate in Kbits/s (default: 32768 for high-bandwidth networks)')
    parser.add_argument('--codec', type=str, default='H264',
                       choices=['H264', 'H265'],
                       help='Streaming codec (default: H264 - potentially slightly faster)')
    parser.add_argument('--serials', nargs='*', type=int,
                       help='Specific camera serial numbers to use (if not provided, uses all available cameras)')
    parser.add_argument('--status-interval', type=int, default=500,
                       help='Frames between status reports per camera (default: 1000 for less overhead)')
    parser.add_argument('--chunk-size', type=int, default=8192,
                       help='Network chunk size in bytes (default: 8192 for low latency, range: 1024-32768)')

    return parser.parse_args()

def get_resolution(res_str):
    """Convert resolution string to ZED SDK resolution enum"""
    res_map = {
        'HD1200': sl.RESOLUTION.HD1200,  # ZED X native resolution: 1920x1200
        'HD1080': sl.RESOLUTION.HD1080,  # 1920x1080
        'SVGA': sl.RESOLUTION.SVGA,      # 960x600
        'None': None,                    # Omit camera
    }
    return res_map.get(res_str, None)

def get_codec(codec_str):
    """Convert codec string to ZED SDK codec enum"""
    codec_map = {
        'H264': sl.STREAMING_CODEC.H264,
        'H265': sl.STREAMING_CODEC.H265
    }
    return codec_map.get(codec_str, sl.STREAMING_CODEC.H264)

def print_device_info(devs, device_type="Camera"):
    """Print information about detected devices"""
    if devs:
        print(f"\n{device_type} devices found:")
        for dev in devs:
            print(f"  ID: {dev.id}, Model: {dev.camera_model}, S/N: {dev.serial_number}, State: {dev.camera_state}")
    else:
        print(f"No {device_type} devices found")

def acquisition(zed, camera_info, args):
    """Acquisition thread function to continuously grab frames"""
    # Pre-allocate all variables to avoid runtime allocations
    camera_model = camera_info['model']
    camera_serial = camera_info['serial']
    camera_port = camera_info['port']
    
    camera_id = f"{camera_model}_SN{camera_serial}"
    
    frame_count = 0
    start_time = time.time()
    
    # Runtime parameters
    runtime_params = sl.RuntimeParameters()
    runtime_params.enable_depth = False
    
    print(f"[{camera_id}] Starting acquisition thread on port {camera_port}")
    
    while not exit_app:
        if zed.grab(runtime_params) == sl.ERROR_CODE.SUCCESS:
            frame_count += 1
            
            # Print status every N frames
            if frame_count % args.status_interval == 0:
                elapsed_time = time.time() - start_time
                fps = frame_count / elapsed_time
                print(f"[{camera_id}] Frames: {frame_count}, FPS: {fps:.1f}")
                # Reset counters for fresh FPS measurement
                frame_count = 0
                start_time = time.time()

    print(f"[{camera_id}] QUIT - disabling streaming and closing camera")
    
    # Cleanup
    zed.disable_streaming()
    zed.close()

def open_camera(zed, serial_number, camera_model, port, args):
    """Open a camera with given serial number and enable streaming with given port"""
    # Determine camera type and create appropriate init parameters
    if isinstance(zed, sl.Camera):
        init_params = sl.InitParameters()
        init_params.depth_mode = sl.DEPTH_MODE.NONE  # Disable depth on sender to save computation
    elif isinstance(zed, sl.CameraOne):
        init_params = sl.CameraOne.InitParametersOne()
    else:
        print(f"Unsupported camera type: {type(zed)}")
        return False, None
    
    # Configure camera parameters - use appropriate resolution based on camera type
    if str(camera_model) == "ZED X":
        resolution_param = args.head_resolution
    elif str(camera_model) == "ZED X Mini":
        resolution_param = args.wrist_resolution
    else:
        print(f"Unsupported camera model: {camera_model}")
        return False, None
    
    resolution = get_resolution(resolution_param)
    if resolution is None:
        print(f"Skipping camera SN{serial_number}")
        return False, None

    if str(camera_model) == "ZED X":
        init_params.camera_resolution = resolution
    elif str(camera_model) == "ZED X Mini":
        init_params.camera_resolution = resolution
    else:
        print(f"Unsupported camera model: {camera_model}")
        return False, None

    init_params.camera_fps = args.fps
    init_params.sdk_verbose = 0  # Always disable for performance
    
    # Ultra-performance optimizations to maximize frames sent without computing extra layers
    init_params.camera_disable_self_calib = True  # Always disable for streaming
    init_params.enable_image_enhancement = False
    init_params.camera_image_flip = sl.FLIP_MODE.OFF
    init_params.enable_image_validity_check = 0
    init_params.async_grab_camera_recovery = True
    init_params.depth_stabilization = 0

    # Set camera parameters
    init_params.set_from_serial_number(serial_number)

    # Open the camera
    open_err = zed.open(init_params)
    if open_err != sl.ERROR_CODE.SUCCESS:
        print(f"ZED SN{serial_number} Error: {open_err}")
        zed.close()
        return False, None

    # Get camera information
    cam_info = zed.get_camera_information()
    print(f"Opened: {cam_info.camera_model}_SN{serial_number}")

    # Configure streaming parameters
    stream_params = sl.StreamingParameters()
    stream_params.codec = get_codec(args.codec)
    stream_params.port = port
    stream_params.bitrate = args.bitrate
    
    stream_params.gop_size = 2  # keyframe interval
    stream_params.adaptative_bitrate = False  # Fixed bitrate for consistency
    stream_params.chunk_size = args.chunk_size
    
    stream_params.target_framerate = args.fps

    # Enable streaming
    stream_err = zed.enable_streaming(stream_params)
    if stream_err != sl.ERROR_CODE.SUCCESS:
        print(f"ZED SN{serial_number} Streaming initialization error: {stream_err}")
        zed.close()
        return False, None

    print(f"  Streaming enabled on port {port}")
    
    # Return camera info for the acquisition thread
    camera_info = {
        'model': cam_info.camera_model,
        'serial': serial_number,
        'port': port
    }
    
    return True, camera_info

def main():
    global exit_app
    
    args = parse_args()
    
    print(f"Starting ZED Multi-Camera Streaming Sender")
    print(f"Head Resolution: {args.head_resolution}, {args.wrist_resolution}")
    print(f"FPS: {args.fps}")
    print(f"Base Port: {args.base_port}")
    print(f"Bitrate: {args.bitrate} Kbits/s")
    print(f"Codec: {args.codec}")
    print(f"Chunk Size: {args.chunk_size} bytes")
    
    # Get the list of available ZED cameras
    dev_stereo_list = sl.Camera.get_device_list()
    print_device_info(dev_stereo_list, "Stereo Camera")
    
    if sys.platform != "win32":
        dev_mono_list = sl.CameraOne.get_device_list()
        print_device_info(dev_mono_list, "Mono Camera")
    else:
        dev_mono_list = []

    # Filter cameras by serial numbers if specified
    if args.serials:
        print(f"\nFiltering cameras by serial numbers: {args.serials}")
        dev_stereo_list = [dev for dev in dev_stereo_list if dev.serial_number in args.serials]
        dev_mono_list = [dev for dev in dev_mono_list if dev.serial_number in args.serials]
        print(f"Filtered to {len(dev_stereo_list)} stereo and {len(dev_mono_list)} mono cameras")

    nb_cameras = len(dev_stereo_list) + len(dev_mono_list)
    if nb_cameras == 0:
        print("No ZED cameras detected or available, exiting")
        return 1

    print(f"\nAttempting to open {nb_cameras} cameras...")

    # Create camera objects and open them
    zeds = []
    camera_infos = []
    threads = []
    cameras_opened = 0

    # Open stereo cameras
    for i, dev in enumerate(dev_stereo_list):
        port = args.base_port + (2 * i)
        zed = sl.Camera()
        success, cam_info = open_camera(zed, dev.serial_number, dev.camera_model, port, args)
        if success:
            zeds.append(zed)
            camera_infos.append(cam_info)
            cameras_opened += 1
        else:
            print(f"Failed to open stereo camera SN{dev.serial_number}")

    # Open mono cameras
    for i, dev in enumerate(dev_mono_list):
        port = args.base_port + (2 * (len(dev_stereo_list) + i))
        zed = sl.CameraOne()
        success, cam_info = open_camera(zed, dev.serial_number, dev.camera_model, port, args)
        if success:
            zeds.append(zed)
            camera_infos.append(cam_info)
            cameras_opened += 1
        else:
            print(f"Failed to open mono camera SN{dev.serial_number}")

    if cameras_opened == 0:
        print("No cameras could be opened, exiting")
        return 1

    print(f"\nSuccessfully opened {cameras_opened} cameras")

    # Create and start acquisition threads
    print("Starting acquisition threads...")
    for i, (zed, cam_info) in enumerate(zip(zeds, camera_infos)):
        thread = threading.Thread(target=acquisition, args=(zed, cam_info, args))
        thread.daemon = True  # Allow main thread to exit even if acquisition threads are running
        threads.append(thread)
        thread.start()

    # Set up signal handler for Ctrl+C
    signal.signal(signal.SIGINT, signal_handler)
    print("All cameras streaming! Press Ctrl+C to stop")

    # Main loop - minimize work here
    try:
        while not exit_app:
            time.sleep(0.1)  # Small sleep to prevent busy waiting
    except KeyboardInterrupt:
        exit_app = True

    # Cleanup
    print("\nStopping all streams...")
    exit_app = True
    time.sleep(0.2)  # Give threads time to see the exit flag

    # Wait for all threads to finish
    print("Waiting for acquisition threads to finish...")
    for thread in threads:
        thread.join(timeout=2.0)  # Wait up to 2 seconds for each thread

    print("All cameras closed successfully")
    return 0

if __name__ == "__main__":
    sys.exit(main())

If anyone has any suggestions or fixes, I’d greatly appreciate it!

Hi Gabriel,

I was wondering if you had any support regarding this issue we’re having.

Best,
Ali

Hi @ali,

Welcome to the forums! :waving_hand:

Thank you for sharing the details of your issue and code example. This helps a lot.

Can you share more information on the frequency of the crashes, as well as the associated logs? Are crashes happening after a few minutes, hours depending on the bitrate?