feat: Phone video bridge (Issue #585) #592

Merged
sl-jetson merged 1 commits from sl-android/issue-585-video-bridge into main 2026-03-14 13:32:25 -04:00
Collaborator

Summary

Two-file implementation: phone-side streaming server + Jetson-side ROS2 receiver.

phone/video_bridge.py — Termux MJPEG streaming server

Feature Detail
Camera backends OpenCV VideoCapture (V4L2) → termux-camera-photo fallback
WebSocket ws://0.0.0.0:8765 — binary JPEG frames; JSON info frame on connect
HTTP MJPEG http://0.0.0.0:8766/streammultipart/x-mixed-replace
HTTP snapshot /snapshot — single JPEG with X-Timestamp header
HTTP health /health — JSON {frames, dropped, width, height, fps}
Resolution 640×480 default; --width/--height/--fps/--quality flags

saltybot_phone/phone_camera_node.py — Jetson ROS2 receiver

Feature Detail
Publishes /saltybot/phone/camera (Image bgr8), /camera/compressed (CompressedImage), /camera/info (String)
Primary transport WebSocket client — binary JPEG, async with websockets
Fallback transport HTTP MJPEG polling (urllib) on WS failure
Latency warning Logs if frame age > latency_warn_ms (default 200 ms)
Diagnostics 10 s timer: received/published counts + last frame age
Reconnect 3 s gap between attempts, both transports

Also: phone_camera_node registered in setup.py; added to phone_bringup.py with phone_host / phone_cam_enabled launch args.

Test plan

  • Phone (Termux): pip install websockets && python3 phone/video_bridge.py --debug
    • Open http://<phone>:8766/stream in browser — see live feed
    • http://<phone>:8766/health returns JSON stats
  • Jetson: ros2 run saltybot_phone phone_camera_node --ros-args -p phone_host:=<ip>
    • ros2 topic hz /saltybot/phone/camera → ~15 Hz
    • ros2 topic echo /saltybot/phone/camera/info → resolution/fps JSON
  • Kill video_bridge.py, wait, restart — Jetson node reconnects
  • ros2 launch saltybot_phone phone_bringup.py phone_host:=<ip> — node starts
  • python3 -m py_compile phone/video_bridge.py — syntax clean

🤖 Generated with Claude Code

## Summary Two-file implementation: phone-side streaming server + Jetson-side ROS2 receiver. ### `phone/video_bridge.py` — Termux MJPEG streaming server | Feature | Detail | |---------|--------| | **Camera backends** | OpenCV VideoCapture (V4L2) → `termux-camera-photo` fallback | | **WebSocket** | `ws://0.0.0.0:8765` — binary JPEG frames; JSON info frame on connect | | **HTTP MJPEG** | `http://0.0.0.0:8766/stream` — `multipart/x-mixed-replace` | | **HTTP snapshot** | `/snapshot` — single JPEG with `X-Timestamp` header | | **HTTP health** | `/health` — JSON `{frames, dropped, width, height, fps}` | | **Resolution** | 640×480 default; `--width`/`--height`/`--fps`/`--quality` flags | ### `saltybot_phone/phone_camera_node.py` — Jetson ROS2 receiver | Feature | Detail | |---------|--------| | **Publishes** | `/saltybot/phone/camera` (Image bgr8), `/camera/compressed` (CompressedImage), `/camera/info` (String) | | **Primary transport** | WebSocket client — binary JPEG, async with `websockets` | | **Fallback transport** | HTTP MJPEG polling (`urllib`) on WS failure | | **Latency warning** | Logs if frame age > `latency_warn_ms` (default 200 ms) | | **Diagnostics** | 10 s timer: received/published counts + last frame age | | **Reconnect** | 3 s gap between attempts, both transports | Also: `phone_camera_node` registered in `setup.py`; added to `phone_bringup.py` with `phone_host` / `phone_cam_enabled` launch args. ## Test plan - [ ] Phone (Termux): `pip install websockets && python3 phone/video_bridge.py --debug` - Open `http://<phone>:8766/stream` in browser — see live feed - `http://<phone>:8766/health` returns JSON stats - [ ] Jetson: `ros2 run saltybot_phone phone_camera_node --ros-args -p phone_host:=<ip>` - `ros2 topic hz /saltybot/phone/camera` → ~15 Hz - `ros2 topic echo /saltybot/phone/camera/info` → resolution/fps JSON - [ ] Kill `video_bridge.py`, wait, restart — Jetson node reconnects - [ ] `ros2 launch saltybot_phone phone_bringup.py phone_host:=<ip>` — node starts - [ ] `python3 -m py_compile phone/video_bridge.py` — syntax clean 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sl-jetson added 1 commit 2026-03-14 12:20:54 -04:00
Phone side — phone/video_bridge.py:
- MJPEG streaming server for Android/Termux phone camera
- Dual camera backends: OpenCV VideoCapture (V4L2) with automatic
  fallback to termux-camera-photo for unmodified Android
- WebSocket server (ws://0.0.0.0:8765) — binary JPEG frames + JSON
  info/error control messages; supports multiple concurrent clients
- HTTP server (http://0.0.0.0:8766):
    /stream    — multipart/x-mixed-replace MJPEG
    /snapshot  — single JPEG
    /health    — JSON stats (frame count, dropped, resolution, fps)
- Thread-safe single-slot FrameBuffer; CaptureThread rate-limited with
  wall-clock accounting for capture latency
- Flags: --ws-port, --http-port, --width, --height, --fps, --quality,
  --device, --camera-id, --no-http, --debug

Jetson side — saltybot_phone/phone_camera_node.py:
- ROS2 node: receives JPEG frames, publishes:
    /saltybot/phone/camera            sensor_msgs/Image (bgr8)
    /saltybot/phone/camera/compressed sensor_msgs/CompressedImage
    /saltybot/phone/camera/info       std_msgs/String (stream metadata)
- WebSocket client (primary); HTTP MJPEG polling fallback on WS failure
- Auto-reconnect loop (default 3 s) for both transports
- Latency warning when frame age > latency_warn_ms (default 200 ms)
- 10 s diagnostics log: received/published counts + last frame age
- Registered as phone_camera_node console script in setup.py
- Added to phone_bringup.py launch with phone_host / phone_cam_enabled args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sl-jetson merged commit 1da1d50171 into main 2026-03-14 13:32:25 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: seb/saltylab-firmware#592
No description provided.