Skip to content

Commit 312e234

Browse files
committed
chore: testing client working
1 parent 2394483 commit 312e234

10 files changed

Lines changed: 179 additions & 109 deletions

File tree

intercomclient/camera_video_stream_track.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import cv2
1+
import asyncio
22
import logging
3-
from intercomclient.config import Config
4-
from aiortc import VideoStreamTrack
3+
54
import av
6-
import asyncio
5+
import cv2
6+
from aiortc import VideoStreamTrack
7+
8+
from intercomclient.config import Config
79

810
LOG = logging.getLogger(__name__)
911

@@ -13,8 +15,9 @@ def __init__(self, config=Config):
1315
self.config = config
1416
self.target_fps = config.target_fps
1517
self.capture = cv2.VideoCapture(config.video_source)
18+
LOG.info("Initialized video capture with source: %s", config.video_source)
1619
if not self.capture.isOpened():
17-
raise ValueError("Unable to open video source")
20+
raise ValueError("Unable to open video source: %s", config.video_source)
1821
self.segment_number = 0
1922
super().__init__()
2023

intercomclient/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from dataclasses import dataclass
2-
from typing import Tuple
3-
from pathlib import Path
41
import logging
52
import os
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
from typing import Tuple
66

77

88
@dataclass()

intercomclient/device_authorization.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from .device import get_device_type, get_os_info
2-
from .config import Config
3-
from time import sleep
1+
import logging
42
from datetime import datetime, timedelta
3+
from time import sleep
4+
55
import requests
66

7+
from .config import Config
8+
from .device import get_device_type, get_os_info
9+
10+
LOG = logging.getLogger(__name__)
11+
712

813
def initiate_device_authorization(config: Config) -> dict:
914
client_id = Config.oauth_client_id
@@ -65,4 +70,10 @@ def refresh_tokens(config: Config, refresh_token) -> dict:
6570
},
6671
)
6772

68-
return api_response.json()
73+
try:
74+
api_response.raise_for_status()
75+
response_json = api_response.json()
76+
return response_json
77+
except requests.HTTPError:
78+
LOG.error("HTTP error during token refresh: %s", api_response.json())
79+
return response_json

intercomclient/token_store.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import datetime
12
import json
23
import logging
34
import os
4-
from .config import Config
5-
import datetime
65
from enum import Enum
76

7+
from .config import Config
8+
89
LOG = logging.getLogger(__name__)
910

1011

intercomclient/video_writer.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from datetime import datetime
2-
import cv2
1+
import json
32
import logging
43
import os
5-
import json
64
import time
5+
from datetime import datetime
76
from pathlib import Path
7+
8+
import cv2
9+
810
from intercomclient.segment import Segment
911

1012
LOG = logging.getLogger(__name__)

main.py

Lines changed: 52 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22
import json
33
import logging
44
import signal
5-
from datetime import datetime, timedelta, UTC
5+
from datetime import UTC, datetime, timedelta
66

77
import websockets
88
from aiortc import RTCPeerConnection, RTCSessionDescription
9+
from requests import HTTPError
910

11+
from intercomclient.camera_video_stream_track import CameraVideoStreamTrack
1012
from intercomclient.config import Config
11-
from intercomclient.token_store import TokenStore, TokenStatus
1213
from intercomclient.device_authorization import (
1314
initiate_device_authorization,
1415
poll_for_token,
1516
refresh_tokens,
1617
)
17-
from intercomclient.camera_video_stream_track import CameraVideoStreamTrack
18+
from intercomclient.token_store import TokenStatus, TokenStore
1819

1920
logging.basicConfig(level=logging.INFO)
2021
LOG = logging.getLogger("pi-client")
@@ -91,10 +92,15 @@ def device_authorization_flow(self):
9192
def refresh_flow(self):
9293
tokens = self.token_store.load_tokens()
9394

94-
refresh_response = refresh_tokens(
95-
self.config,
96-
tokens["refresh"]["token_value"],
97-
)
95+
try:
96+
refresh_response = refresh_tokens(
97+
self.config,
98+
tokens["refresh"]["token_value"],
99+
)
100+
101+
except HTTPError as e:
102+
LOG.error("Token refresh failed: %s", e)
103+
print(refresh_response)
98104

99105
expiry = (
100106
datetime.now(tz=UTC) + timedelta(seconds=refresh_response["expires_in"])
@@ -121,9 +127,6 @@ async def on_connectionstatechange():
121127
if self.pc.connectionState in ("failed", "closed"):
122128
await self.shutdown()
123129

124-
camera_track = CameraVideoStreamTrack(self.config)
125-
self.pc.addTrack(camera_track)
126-
127130
async def signaling_loop(self):
128131
token_store = self.token_store.load_tokens()
129132
access_token = token_store["access"]["token_value"]
@@ -142,48 +145,57 @@ async def signaling_loop(self):
142145

143146
await self.setup_peer_connection()
144147

145-
# Create offer
146-
# offer = await self.pc.createOffer()
147-
# await self.pc.setLocalDescription(offer)
148-
149-
# await ws.send(
150-
# json.dumps(
151-
# {
152-
# "type": "offer",
153-
# "sdp": self.pc.localDescription.sdp,
154-
# }
155-
# )
156-
# )
148+
@self.pc.on("icecandidate")
149+
async def on_icecandidate(self, candidate):
150+
print(f"ICE candidate generated: {candidate}")
151+
if candidate is not None:
152+
# Send the candidate to the signaling server
153+
await self.ws.send_json(
154+
json.dumps(
155+
{
156+
"type": "ice",
157+
"candidate": {
158+
"candidate": candidate.component,
159+
"sdpMid": candidate.sdpMid,
160+
"sdpMLineIndex": candidate.sdpMLineIndex,
161+
"foundation": candidate.foundation,
162+
"priority": candidate.priority,
163+
"ip": candidate.address,
164+
"port": candidate.port,
165+
"protocol": candidate.protocol,
166+
"type": candidate.type,
167+
},
168+
}
169+
)
170+
)
157171

158172
async for message in ws:
159173
data = json.loads(message)
160174
print(f"Received message: {data}")
161175

162176
if data["type"] == "offer":
163-
try:
164-
await self.pc.setRemoteDescription(
165-
RTCSessionDescription(
166-
sdp=data["sdp"],
167-
type=data["type"],
168-
)
177+
await self.pc.setRemoteDescription(
178+
RTCSessionDescription(
179+
sdp=data["sdp"],
180+
type=data["type"],
169181
)
170-
print("Remote description set successfully")
171-
except Exception as e:
172-
print("setRemoteDescription failed:", e)
173-
raise
182+
)
174183

175184
camera_track = CameraVideoStreamTrack(self.config)
176-
await self.pc.addTrack(camera_track)
177-
178-
print("Sending answer...")
185+
self.pc.addTrack(camera_track)
179186
answer = await self.pc.createAnswer()
187+
180188
await self.pc.setLocalDescription(answer)
181-
await self.ws.send_json(
182-
{
183-
"type": self.pc.localDescription.type,
184-
"sdp": self.pc.localDescription.sdp,
185-
}
189+
print(self.pc.localDescription.type)
190+
await self.ws.send(
191+
json.dumps(
192+
{
193+
"type": self.pc.localDescription.type,
194+
"sdp": self.pc.localDescription.sdp,
195+
}
196+
)
186197
)
198+
print("Answer sent successfully")
187199

188200
elif data["type"] == "ice":
189201
await self.pc.addIceCandidate(data["candidate"])

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ dev = [
2626

2727
[tool.uv.sources]
2828
transparenc-sdk = { url = "https://github.com/kieranmanning/IntercomClientSDKs/releases/download/v0.9/transparenc_sdk-1.0.0-py3-none-any.whl" }
29+
30+
[tool.ruff]
31+
lint.extend-select = ["I"]

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[pytest]
2-
addopts = -s -v
2+
addopts = -s - v

tests/integration/client.py

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,97 @@
1+
import os
2+
3+
os.environ["QT_QPA_PLATFORM"] = "xcb"
4+
15
import asyncio
26
import json
7+
8+
import cv2
9+
import websockets
310
from aiortc import RTCPeerConnection, RTCSessionDescription
4-
from intercomclient.token_store import TokenStore
11+
512
from intercomclient.config import Config
6-
import websockets
13+
from intercomclient.token_store import TokenStore
14+
715

16+
class TestClient:
17+
def __init__(self):
18+
self.pc = RTCPeerConnection()
19+
self.pc.addTransceiver("video", direction="recvonly")
20+
self.config = Config()
21+
self.output_dir = "/tmp/intercom_client_testing/frames/"
22+
if not os.path.exists(self.output_dir):
23+
os.makedirs(self.output_dir)
24+
self.device_code = TokenStore(self.config, verify=False).load_tokens()[
25+
"device_code"
26+
]
27+
self.websocket_api_url = (
28+
f"{self.config.websocket_api_base_url}/{self.device_code}/"
29+
)
30+
self.remote_video = None
31+
32+
async def show_frame(self, frame):
33+
await asyncio.to_thread(cv2.imshow, "Remote CCTV Feed", frame)
34+
35+
async def display_remote_video(self):
36+
print("Displaying remote video track...")
37+
track = self.remote_video
38+
self.frame_count = 0
39+
while True:
40+
track = self.remote_video
41+
if track is None:
42+
await asyncio.sleep(1)
43+
print("Waiting for remote video track to be available...")
44+
continue
45+
print("Waiting for video frame...")
46+
frame = await track.recv() # receive an AV frame
47+
print("Video frame received, converting to OpenCV format...")
48+
img = frame.to_ndarray(format="bgr24") # convert to OpenCV format
49+
print("Displaying video frame...")
50+
frame_path = os.path.join(self.output_dir, "frame.jpg")
51+
if self.frame_count % 3 == 0: # every 3rd frame
52+
cv2.imwrite(frame_path, img)
53+
self.frame_count += 1
54+
55+
async def test_frame(self):
56+
@self.pc.on("track")
57+
async def on_track(track):
58+
print("Track received:", track.kind)
859

9-
async def test_frame():
10-
pc = RTCPeerConnection()
11-
config = Config()
12-
device_code = TokenStore(config, verify=False).load_tokens()["device_code"]
13-
websocket_api_url = f"{config.websocket_api_base_url}/{device_code}/"
14-
15-
# # Optionally add a dummy track if server expects one
16-
# class DummyTrack(VideoStreamTrack):
17-
# async def recv(self):
18-
# # just return None for testing
19-
# return None
20-
21-
# pc.addTrack(DummyTrack())
22-
23-
async with websockets.connect(websocket_api_url) as ws:
24-
# Create offer
25-
offer = await pc.createOffer()
26-
await pc.setLocalDescription(offer)
27-
28-
await ws.send(json.dumps({"type": "offer", "sdp": pc.localDescription.sdp}))
29-
30-
# Wait for answer from server
31-
async for message in ws:
32-
print(f"Test client - Received message: {message}")
33-
data = json.loads(message)
34-
if data["type"] == "answer":
35-
await pc.setRemoteDescription(
36-
RTCSessionDescription(sdp=data["sdp"], type="answer")
37-
)
38-
elif data["type"] == "ice":
39-
await pc.addIceCandidate(data["candidate"])
40-
41-
print("WebRTC handshake done!")
42-
43-
# Wait for first frame from server
44-
@pc.on("track")
45-
def on_track(track):
46-
print(f"Track received: {track.kind}")
4760
if track.kind == "video":
48-
# grab a single frame
49-
async def grab_frame():
50-
frame = await track.recv()
51-
print(f"Received frame: {frame}")
52-
await pc.close()
53-
return frame
61+
self.remote_video = track
62+
63+
async with websockets.connect(self.websocket_api_url) as ws:
64+
# Create offer
65+
offer = await self.pc.createOffer()
66+
await self.pc.setLocalDescription(offer)
67+
68+
await ws.send(
69+
json.dumps({"type": "offer", "sdp": self.pc.localDescription.sdp})
70+
)
71+
72+
# Wait for answer from server
73+
async for message in ws:
74+
print(f"Test client - Received message: {message}")
75+
data = json.loads(message)
76+
if data["type"] == "answer":
77+
await self.pc.setRemoteDescription(
78+
RTCSessionDescription(sdp=data["sdp"], type="answer")
79+
)
80+
elif data["type"] == "ice":
81+
await self.pc.addIceCandidate(data["candidate"])
82+
print("WebRTC handshake done!")
83+
84+
# Keep the event loop alive until frame is received
85+
await asyncio.sleep(5)
86+
87+
88+
async def main():
89+
test_client = TestClient()
5490

55-
asyncio.create_task(grab_frame())
91+
signaling_task = asyncio.create_task(test_client.test_frame())
92+
display_task = asyncio.create_task(test_client.display_remote_video())
5693

57-
# Keep the event loop alive until frame is received
58-
await asyncio.sleep(5)
94+
await asyncio.gather(signaling_task, display_task)
5995

6096

61-
asyncio.run(test_frame())
97+
asyncio.run(main())

0 commit comments

Comments
 (0)