Small Python app that finds connected cameras and shows all working camera feeds in one window.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtpython camera_viewer.pyFor several USB cameras on the same hub, start by listing the detected capture nodes:
python3 camera_viewer.py --listTo inspect negotiated camera modes and read failures:
python3 camera_viewer.py --probe --width 320 --height 240 --fps 10 --fourcc MJPGThen run with MJPEG and modest FPS to reduce USB bandwidth:
python3 camera_viewer.py --width 640 --height 480 --fps 15 --fourcc MJPGThe default six-camera layout is:
video4=fL video10=f video8=fR
video6=bL video0=b video2=bR
To override it:
python3 camera_viewer.py --layout "4=fL,10=f,8=fR/6=bL,0=b,2=bR"If some cameras do not open, try a lower bandwidth mode:
python3 camera_viewer.py --width 320 --height 240 --fps 10 --fourcc MJPGFor aligned recording, use --record-dir. The app captures each cycle with
grab() across all cameras first, then retrieve() for decoding. This reduces
software timestamp skew compared with reading one full frame at a time.
python3 camera_viewer.py \
--width 640 --height 480 --fps 15 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 60 --warmup-seconds 1.5For lowest display overhead during collection:
python3 camera_viewer.py \
--width 640 --height 480 --fps 15 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 60 --warmup-seconds 1.5 --no-previewTo record a UDP LiDAR stream in the same session, add the LiDAR UDP port:
python3 camera_viewer.py \
--width 320 --height 240 --fps 10 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 30 --warmup-seconds 1.5 --no-preview \
--lidar-udp-port 2368The LiDAR recorder uses the same monotonic_ns clock as camera metadata. It
stores raw packets in lidar_udp.bin and packet timestamps in lidar_udp.csv.
Use --lidar-bind if the LiDAR must bind to a specific local interface IP.
If zero LiDAR packets are recorded, probe common LiDAR UDP ports first:
python3 probe_lidar_udp.py --duration 10Loopback sources such as 127.0.0.1 are local machine traffic, not the LiDAR,
and are ignored by default.
If your LiDAR-facing interface has a fixed IP, bind to it:
python3 probe_lidar_udp.py --bind 192.168.1.100 --duration 10When the probe finds packets, use that destination port as --lidar-udp-port.
You can also probe a custom port range:
python3 probe_lidar_udp.py --ports 2000-9000 --duration 10If no UDP packets appear, sniff the physical interface to discover whether any UDP traffic is arriving at all:
sudo python3 sniff_udp.py eno1 --duration 10
sudo python3 sniff_udp.py usb0 --duration 10Use the destination port printed by sniff_udp.py as --lidar-udp-port.
For the current AGX setup, LiDAR traffic has been seen on eno1 from
169.254.213.23 to local ports 47131 and 46075.
For cleaner LiDAR-only sniffing:
sudo python3 sniff_udp.py eno1 --source-ip 169.254.213.23 --duration 10For this setup, record both LiDAR UDP streams with:
python3 camera_viewer.py \
--width 320 --height 240 --fps 10 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 30 --warmup-seconds 1.5 --no-preview \
--lidar-bind 169.254.1.100 \
--lidar-udp-port 47131 --lidar-udp-port 46075For timing tests with lower disk load, record LiDAR metadata only:
python3 camera_viewer.py \
--width 320 --height 240 --fps 10 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 30 --warmup-seconds 1.5 --no-preview \
--lidar-bind 169.254.1.100 \
--lidar-udp-port 47131 --lidar-udp-port 46075 \
--lidar-metadata-onlyFor full raw LiDAR payloads, omit --lidar-metadata-only. Raw packets are sent
to a separate writer process through a bounded queue so disk I/O is isolated
from camera capture:
python3 camera_viewer.py \
--width 320 --height 240 --fps 10 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 30 --warmup-seconds 1.5 --no-preview \
--lidar-bind 169.254.1.100 \
--lidar-udp-port 47131 --lidar-udp-port 46075 \
--lidar-writer-queue 2048Check raw_dropped in analyze_lidar.py. If it is nonzero, the raw LiDAR
writer could not keep up; increase --lidar-writer-queue or write to faster
storage.
The recorder grabs cameras in parallel by default to reduce software timestamp spread. To compare against one-at-a-time grabbing:
python3 camera_viewer.py \
--width 640 --height 480 --fps 15 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 10 --warmup-seconds 1.5 --no-preview --sequential-grabIf one of the expected layout cameras is missing, the app will print it. For
example, missing f (video10) during recording usually means bandwidth or
device-open contention. Try this lower-bandwidth six-camera test:
python3 camera_viewer.py \
--width 320 --height 240 --fps 10 --fourcc MJPG --buffer-size 1 \
--record-dir data --duration 30 --warmup-seconds 1.5 --no-previewYour first few cycles may have startup skew while cameras settle. By default,
--warmup-seconds 1.5 discards paced capture cycles before recording starts.
Each recording creates a timestamped session directory containing:
manifest.json: camera labels, sources, FPS, and format.metadata.csv: one row per camera per capture cycle withmonotonic_ns,system_ns,frame_index, andskew_ns.- One
.avifile per camera. - Optional
lidar_udp.csvandlidar_udp.binfiles when--lidar-udp-portis used.
Summarize a run with:
python3 analyze_recording.py data/session_YYYYMMDD_HHMMSSCheck whether alignment is good enough:
python3 check_alignment.py data/session_YYYYMMDD_HHMMSSSummarize LiDAR packets:
python3 analyze_lidar.py data/session_YYYYMMDD_HHMMSSCreate an HTML LiDAR timing/rate visualization:
python3 visualize_lidar_timing.py data/session_YYYYMMDD_HHMMSSOpen lidar_timing.html from that session in a browser.
Point-cloud visualization requires raw LiDAR packets, not --lidar-metadata-only,
plus the LiDAR model/packet decoder.
Export raw LiDAR packets to PCAP for vendor tools:
python3 export_lidar_pcap.py data/session_YYYYMMDD_HHMMSS --dst-ip 169.254.1.100Open lidar.pcap in the LiDAR vendor viewer. Use the model-specific viewer or
SDK, for example Ouster Studio, Hesai PandarView, Velodyne VeloView, or the
vendor's ROS driver.
To list timing outliers above a custom threshold:
python3 analyze_recording.py data/session_YYYYMMDD_HHMMSS --outlier-ms 5To evaluate an older run after dropping startup cycles:
python3 analyze_recording.py data/session_YYYYMMDD_HHMMSS --ignore-start-cycles 12monotonic_ns is the best timestamp for aligning streams from this process.
skew_ns is the measured software timestamp spread across cameras in a cycle.
For true sub-frame synchronization, the cameras need hardware trigger/sync
support; USB webcams cannot guarantee that by software alone.
Optional flags:
python camera_viewer.py --max-index 12 --width 640 --height 480 --fps 15Controls:
- Press
qorEscto quit. - If no cameras appear, make sure your user can access video devices such as
/dev/video0.
If python3 -m venv .venv was interrupted, delete the partial .venv folder and create it again:
rm -rf .venv
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtIf pip install -r requirements.txt already succeeded with "Defaulting to user installation",
you can run the app without activating a virtualenv:
python3 camera_viewer.pyOpenCV or V4L2 warnings during startup usually mean the system exposed a video node that is not a normal camera stream. They are not fatal unless no preview window opens.
Many UVC cameras expose two /dev/video* nodes. The app filters for primary capture
nodes so six physical cameras should normally show as six previews, not twelve.