Skip to content

Commit 2394483

Browse files
committed
feat: webRTC
1 parent df219eb commit 2394483

14 files changed

Lines changed: 795 additions & 100 deletions

.envrc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
export OAUTH_CLIENT_ID=7v4bnRCzelHg6DdEtw6K81ayjTP3GJV0seFtsRv8
2-
export TOKEN_FILE_PATH=".token-file.json"
3-
export API_URI="http://localhost:8000"
1+
export OAUTH_CLIENT_ID=K7asmp280fyVAfgx12uCcqauaVIFnb2Py9DpwCn1
2+
export VIDEO_SOURCE=0
3+
export TOKEN_FILE_PATH="~/.config/intercom-client/tokens.json"
4+
export WEBSOCKET_API_BASE_URL="ws://localhost:8000/ws/live_stream"
5+
export HTTP_API_BASE_URL="http://localhost:8000"
6+
7+
source .venv/bin/activate

.token-file.json

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import cv2
2+
import logging
3+
from intercomclient.config import Config
4+
from aiortc import VideoStreamTrack
5+
import av
6+
import asyncio
7+
8+
LOG = logging.getLogger(__name__)
9+
10+
11+
class CameraVideoStreamTrack(VideoStreamTrack):
12+
def __init__(self, config=Config):
13+
self.config = config
14+
self.target_fps = config.target_fps
15+
self.capture = cv2.VideoCapture(config.video_source)
16+
if not self.capture.isOpened():
17+
raise ValueError("Unable to open video source")
18+
self.segment_number = 0
19+
super().__init__()
20+
21+
async def recv(self):
22+
pts, time_base = await self.next_timestamp()
23+
ret, frame = self.capture.read()
24+
if not ret:
25+
await asyncio.sleep(1 / self.target_fps)
26+
return await self.recv()
27+
28+
frame = av.VideoFrame.from_ndarray(frame, format="bgr24")
29+
frame.pts = pts
30+
frame.time_base = time_base
31+
return frame

intercomclient/config.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
from dataclasses import dataclass
22
from typing import Tuple
3+
from pathlib import Path
34
import logging
45
import os
56

67

78
@dataclass()
89
class Config:
910
segment_duration: int = 60
10-
fps: int = 5
11+
target_fps: int = 5
1112
resolution: Tuple[int, int] = (320, 240)
1213
debug_mode: bool = True
13-
video_source: int = 0
14+
video_source: int = int(os.getenv("VIDEO_SOURCE", 0))
1415
output_dir_path: str = "/tmp/intercom_videos"
1516
fourcc: str = "XVID"
1617
video_format: str = "avi"
1718
oauth_scope: str = "openid email profile"
1819
oauth_grant: str = "urn:ietf:params:oauth:grant-type:device_code"
1920
oauth_client_id: str = os.getenv("OAUTH_CLIENT_ID", "wrong")
20-
token_file_path: str = os.getenv("TOKEN_FILE_PATH", "wrong")
21-
api_uri: str = os.getenv("API_URI", "wrong")
21+
token_file_path: Path = Path(
22+
os.getenv("TOKEN_FILE_PATH", "~/.config/intercomclient/tokens.json")
23+
).expanduser()
24+
http_api_base_url: str = os.getenv("HTTP_API_BASE_URL", "wrong")
25+
websocket_api_base_url: str = os.getenv("WEBSOCKET_API_BASE_URL", "wrong")
2226
max_polling_time_mins: int = int(os.getenv("MAX_POLLING_TIME_MINS", 5))
2327

2428

intercomclient/device.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33

44
def get_os_info():
5-
return platform.platform()
5+
return str(platform.platform())[0:30]
66

77

88
def get_device_type():
9-
return platform.machine()
9+
return str(platform.machine())[0:30]

intercomclient/device_authorization.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def initiate_device_authorization(config: Config) -> dict:
99
client_id = Config.oauth_client_id
1010
device_type = get_device_type()
1111
device_os = get_os_info()
12-
url = f"{Config.api_uri}/api/v1/oauth/device-authorization/"
12+
url = f"{config.http_api_base_url}/oauth/device-authorization/"
1313

1414
api_response = requests.post(
1515
url,
@@ -24,23 +24,24 @@ def initiate_device_authorization(config: Config) -> dict:
2424

2525

2626
def poll_for_token(config: Config, device_code: str, interval: int) -> dict:
27-
client_id = Config.oauth_client_id
27+
client_id = config.oauth_client_id
2828

2929
continue_polling = True
3030
polling_end_time = datetime.now() + timedelta(minutes=config.max_polling_time_mins)
31-
url = f"{Config.api_uri}/oauth/token/"
31+
url = f"{config.http_api_base_url}/oauth/token/"
3232
while continue_polling:
3333
if datetime.now() > polling_end_time:
3434
continue_polling = False
3535
break
3636
sleep(interval)
37+
post_data = {
38+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
39+
"device_code": device_code,
40+
"client_id": client_id,
41+
}
3742
api_response = requests.post(
3843
url,
39-
data={
40-
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
41-
"device_code": device_code,
42-
"client_id": client_id,
43-
},
44+
data=post_data,
4445
)
4546
resp_json = api_response.json()
4647
if "error" in resp_json and resp_json["error"] == "authorization_pending":
@@ -53,7 +54,7 @@ def poll_for_token(config: Config, device_code: str, interval: int) -> dict:
5354

5455
def refresh_tokens(config: Config, refresh_token) -> dict:
5556
client_id = Config.oauth_client_id
56-
url = f"{Config.api_uri}/oauth/token/"
57+
url = f"{config.http_api_base_url}/oauth/token/"
5758

5859
api_response = requests.post(
5960
url,

intercomclient/local_store.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

intercomclient/token_store.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
import logging
3+
import os
4+
from .config import Config
5+
import datetime
6+
from enum import Enum
7+
8+
LOG = logging.getLogger(__name__)
9+
10+
11+
class TokenStatus(Enum):
12+
VALID = 1
13+
EXPIRED = 2
14+
MISSING = 3
15+
EXPIRING = 4
16+
17+
18+
class CannotLoadTokensException(Exception):
19+
pass
20+
21+
22+
class TokenStore:
23+
"""
24+
Class to manage token storage and access
25+
"""
26+
27+
def __init__(self, config: Config, verify=True):
28+
self.config = config
29+
30+
if verify:
31+
self.verify_token_file_path()
32+
33+
@classmethod
34+
def check_token_expiry_delta(cls, token_expiry_time) -> datetime.timedelta:
35+
current_time = datetime.datetime.now().timestamp()
36+
return datetime.timedelta(seconds=current_time - token_expiry_time)
37+
38+
def load_tokens(self):
39+
self.verify_token_file_path()
40+
with open(self.config.token_file_path, "r") as token_file_handle:
41+
token_file_json = json.load(token_file_handle)
42+
try:
43+
access_token = token_file_json["access"]["token_value"]
44+
refresh_token = token_file_json["refresh"]["token_value"]
45+
access_token_expiry = token_file_json["access"]["expiry_time"]
46+
device_code = token_file_json["device_code"]
47+
48+
return {
49+
"access": {
50+
"token_value": access_token,
51+
"expiry_time": access_token_expiry,
52+
},
53+
"refresh": {"token_value": refresh_token},
54+
"device_code": device_code,
55+
}
56+
57+
except KeyError as e:
58+
error_message = f"Token file is missing keys, will assume empty {self.config.token_file_path}"
59+
raise CannotLoadTokensException(error_message) from e
60+
61+
def store_tokens(self, tokens: dict[str, str], access_token_expiry, device_code):
62+
self.verify_token_file_path()
63+
64+
data = {
65+
"access": {
66+
"token_value": tokens["access_token"],
67+
"expiry_time": access_token_expiry,
68+
},
69+
"refresh": {"token_value": tokens["refresh_token"]},
70+
"device_code": device_code,
71+
}
72+
with open(self.config.token_file_path, "w") as token_file_handle:
73+
token_file_handle.write(json.dumps(data))
74+
75+
def verify_token_file_path(self) -> bool:
76+
token_file_path = self.config.token_file_path
77+
token_parent_dir_path = os.path.split(token_file_path)[0]
78+
# Check our dir path exists, and if not attempt to create it
79+
if not os.path.isdir(token_parent_dir_path):
80+
try:
81+
os.makedirs(token_parent_dir_path, exist_ok=True)
82+
except OSError as e:
83+
error_message = f"Error: could not find or create token file parent directory: {token_file_path}"
84+
raise OSError(error_message) from e
85+
86+
# Check our file exists and has the correct permissions
87+
if not os.path.exists(token_file_path):
88+
token_fd = os.open(
89+
token_file_path,
90+
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
91+
0o600,
92+
)
93+
example_file_outline = {
94+
"access": {"token_value": None, "expiry_time": None},
95+
"refresh": {"token_value": None},
96+
"device_code": None,
97+
}
98+
with os.fdopen(token_fd, "w") as file_handle:
99+
file_handle.write(json.dumps(example_file_outline))
100+
else:
101+
# If the file already exists, we want to check it has the correct permissions
102+
current_permissions = oct(os.stat(token_file_path).st_mode)[-3:]
103+
if current_permissions != "600":
104+
error_message = f"Error: token file has incorrect permissions: {token_file_path}, current permissions: {current_permissions}"
105+
raise OSError(error_message)
106+
107+
return True

intercomclient/video_pipeline.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)