Skip to content

Commit 105295f

Browse files
committed
feat: baking version into CI
1 parent 232eb02 commit 105295f

2 files changed

Lines changed: 42 additions & 14 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ jobs:
272272
run: |
273273
IMAGE=docker.kmanning.ie:5000/intercom-python-client
274274
docker build \
275-
--build-arg APP_VERSION="$TAG" \
275+
--build-arg APP_VERSION="${{ needs.bump.outputs.new_version }}" \
276276
-f Dockerfile \
277277
-t "$IMAGE:latest" \
278278
-t "$IMAGE:${{ github.sha }}" \

CLAUDE.md

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Intercom ClientPython
22

3-
Python device client that streams video from a camera to a remote receiver (browser viewer) using WebRTC, authenticated via OAuth 2.0 Device Authorization Grant.
3+
Python device client that streams video from a camera to a remote viewer (browser) using WebRTC, authenticated via OAuth 2.0 Device Authorization Grant, with continuous telemetry reporting.
44

55
## Tech Stack
66

77
- **Python 3.14+** with **uv** package manager
88
- **aiortc** — WebRTC peer connection and media streaming
9+
- **aioice** — ICE candidate handling (used internally by aiortc; `candidate_from_sdp` from `aiortc.sdp` parses ICE SDP strings)
910
- **OpenCV (cv2)** — Camera capture
1011
- **websockets** — WebSocket signaling client
11-
- **boto3** — AWS SDK (indirectly via backend)
12-
- **requests** — HTTP API calls for OAuth flow
12+
- **requests** — HTTP API calls for OAuth flow and telemetry
1313

1414
## Project Structure
1515

@@ -20,7 +20,8 @@ ClientPython/
2020
│ ├── config.py # Config dataclass (env vars)
2121
│ ├── device_authorization.py # OAuth device flow functions
2222
│ ├── token_store.py # Token persistence (JSON file, 0600 perms)
23-
│ └── camera_video_stream_track.py # WebRTC video track (OpenCV capture)
23+
│ ├── camera_video_stream_track.py # WebRTC video track (OpenCV capture)
24+
│ └── telemetry.py # TelemetryClient — posts events to API
2425
├── pyproject.toml # uv dependencies
2526
└── Dockerfile.dev # Dev image (bookworm-slim + OpenCV deps)
2627
```
@@ -32,24 +33,50 @@ ClientPython/
3233
- `initiate_device_authorization()` — POSTs to `/oauth/device-authorization/` with device type/OS
3334
- `poll_for_token()` — Polls `/oauth/token/` with device code until approved (timeout configurable via `MAX_POLLING_TIME_MINS`, default 5 min)
3435
- `refresh_tokens()` — Refreshes access token using stored refresh token
35-
- `TokenStore` — Stores access/refresh tokens + device_code to JSON file (permissions `0600`) at `~/.config/intercomclient/tokens.json`
36+
- `TokenStore` — Stores access/refresh tokens + `device_code` to JSON file (permissions `0600`) at `~/.config/intercomclient/tokens.json`
3637

3738
### WebRTC Signaling (`main.py``PiClient`)
3839
- Connects to WebSocket signaling server at `{WEBSOCKET_API_BASE_URL}/ws/live_stream/{device_code}/`
39-
- Authenticates with Bearer token in `Authorization` header
40+
- Authenticates with OAuth2 Bearer token in `Authorization` header
4041
- **Signaling loop** handles:
4142
- `offer` (from viewer) — Sets remote SDP, creates `CameraVideoStreamTrack`, generates SDP answer, sends it back
42-
- `answer` (unused — device is answerer only)
43-
- `candidate` — Receives ICE candidates from remote peer; `sdpMid`/`sdpMLineIndex` are top-level fields in the message
44-
- `status``peer_connected`/`peer_disconnected` events
43+
- `candidate` — Receives ICE candidates; parsed with `candidate_from_sdp()` from `aiortc.sdp` (strips `candidate:` prefix; sets `sdpMid`/`sdpMLineIndex` manually as they are top-level message fields, not part of the SDP string)
44+
- `status` with `event: "peer_disconnected"` and `peer_type != "device"` — viewer disconnected; resets RTCPeerConnection in-place (signaling WS stays open) ready for the next offer
4545
- `icecandidate` events (local) — Sends locally-generated ICE candidates back through signaling server
46-
- Connection lifecycle: shutdown on `failed`/`closed`, retry on errors with 5s backoff
46+
- On WebRTC `connectionState == "connected"`, fires a `streaming` telemetry event
47+
- On WebRTC `connectionState == "failed"`, fires an `error` telemetry event then resets the PC
48+
49+
### PC Lifecycle (`setup_peer_connection`)
50+
- Nulls out `self.pc` **before** closing the old one — prevents stale `connectionstatechange` events from the closing PC from affecting the new connection
51+
- Uses `if pc is not self.pc: return` guard in all event handlers to discard events from replaced PCs
52+
- Signaling WebSocket is **never** closed by WebRTC state changes — only by unrecoverable signaling errors
4753

4854
### Video Capture (`intercomclient/camera_video_stream_track.py`)
4955
- `CameraVideoStreamTrack` extends `aiortc.VideoStreamTrack`
5056
- Uses `cv2.VideoCapture` to capture frames from camera source (default device `0`, configurable via `VIDEO_SOURCE`)
5157
- Converts frames to `av.VideoFrame` (BGR24 format) with proper PTS/time_base timestamps
52-
- Retries silently on capture failure
58+
- `recv()` loops until a frame is captured (no recursion)
59+
- `stop()` releases `VideoCapture` to avoid device handle leaks when the PC is replaced
60+
61+
### Telemetry (`intercomclient/telemetry.py``TelemetryClient`)
62+
- `send(event, message="", level="INFO")` — synchronous; called via `asyncio.to_thread` from async contexts
63+
- Reads the current access token and `device_code` from `TokenStore` on each call
64+
- POSTs to `POST /api/v1/devices/{device_code}/telemetry/` with `Authorization: Bearer <token>`
65+
- Failures are logged at DEBUG and silently ignored — telemetry never disrupts the signaling loop
66+
67+
**Events sent automatically:**
68+
69+
| Event | When |
70+
|---|---|
71+
| `connected` | Immediately after signaling WS connects and first PC is set up |
72+
| `streaming` | WebRTC `connectionState` transitions to `"connected"` |
73+
| `disconnected` | Viewer disconnects (`peer_disconnected` status message) |
74+
| `connected` | After PC reset following viewer disconnect (ready for next viewer) |
75+
| `error` (WARNING) | Camera unavailable when adding track |
76+
| `error` (WARNING) | WebRTC `connectionState` transitions to `"failed"` |
77+
| `error` (ERROR) | Exception propagates out of `signaling_loop` |
78+
| `disconnected` | `shutdown()` called (SIGINT/SIGTERM) |
79+
| `heartbeat` | Every 30 seconds from background `_heartbeat_loop` task |
5380

5481
### Configuration (`intercomclient/config.py`)
5582
- All configurable via environment variables:
@@ -62,8 +89,9 @@ ClientPython/
6289
- Defaults: 320×240 resolution, 5 fps
6390

6491
### Entry Point (`main.py``main()`)
65-
- Creates `PiClient`, registers SIGINT/SIGTERM handlers
66-
- Runs client in a loop: ensure valid tokens → connect signaling → stream
92+
- Creates `PiClient` with `TelemetryClient` attached
93+
- Registers SIGINT/SIGTERM handlers calling `shutdown()`
94+
- `run()` starts the 30s heartbeat background task, then loops: ensure valid tokens → connect signaling → stream; sleeps 5s and retries on error
6795

6896
## Docker Dev Setup
6997

0 commit comments

Comments
 (0)