Skip to content

Commit 3e15d14

Browse files
committed
Requirements.txt moved to parent folder, types adjsuted
Signed-off-by: anuunchin <88698977+anuunchin@users.noreply.github.com>
1 parent 18cbeca commit 3e15d14

9 files changed

Lines changed: 54 additions & 45 deletions

File tree

Makefile

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: help lint lint-frontend lint-backend type-check-backend test test-frontend test-backend format format-frontend format-backend docker-build docker-build-frontend docker-build-backend docker-run-frontend docker-run-backend docker-stop docker-clean
1+
.PHONY: help lint lint-frontend lint-backend test test-frontend test-backend format format-frontend format-backend docker-build docker-build-frontend docker-build-backend docker-run-frontend docker-run-backend docker-stop docker-clean
22

33
help:
44
@echo "make"
@@ -14,8 +14,6 @@ help:
1414
@echo " lints frontend code with npm run lint"
1515
@echo " lint-backend"
1616
@echo " lints backend Python code with ruff"
17-
@echo " type-check-backend"
18-
@echo " type checks backend Python code with mypy"
1917
@echo " format"
2018
@echo " formats all code (frontend and backend)"
2119
@echo " format-frontend"
@@ -55,13 +53,14 @@ install-backend:
5553
cd src/backend && uv pip install -r requirements.txt
5654
cd src/backend && uv pip install -r requirements-dev.txt
5755

58-
lint: lint-frontend lint-backend type-check-backend
56+
lint: lint-frontend lint-backend
5957

6058
lint-frontend:
6159
cd src/frontend && npm run lint
6260

6361
lint-backend:
6462
cd src/backend && uv run ruff check .
63+
cd src/backend && uv run mypy .
6564

6665
format: format-frontend format-backend
6766

@@ -71,9 +70,6 @@ format-frontend:
7170
format-backend:
7271
cd src/backend && uv run ruff format .
7372

74-
type-check-backend:
75-
cd src/backend && uv run mypy .
76-
7773
test: test-frontend test-backend
7874

7975
test-frontend:

src/backend/requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fastapi==0.115.10
2+
aiortc==1.14.0
3+
av==16.0.1
4+
opencv-python==4.12.0.88
5+
pydantic==2.12.3
6+
uvicorn==0.38.0
7+
numpy==2.2.6

src/backend/webrtc/requirements.txt

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

src/backend/webrtc/server.py

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
- Wildcard origins without credentials (spec-compliant).
99
1010
Run:
11-
uvicorn server:app --host 0.0.0.0 --port 8000
11+
uv run uvicorn server:app --host 0.0.0.0 --port 8000
1212
"""
1313

1414
import asyncio
1515
import os
1616
import sys
1717
import contextlib
18-
from typing import Optional, List
18+
from typing import Optional, List, Tuple
1919

20+
import numpy as np
2021
import cv2
2122
from fastapi import FastAPI, HTTPException, Response
2223
from fastapi.middleware.cors import CORSMiddleware
@@ -28,22 +29,24 @@
2829
RTCIceServer,
2930
VideoStreamTrack,
3031
)
32+
from av import VideoFrame
33+
3134

3235
# -----------------------------
3336
# Global camera singleton
3437
# -----------------------------
3538

3639

3740
class _SharedCamera:
38-
def __init__(self):
41+
def __init__(self) -> None:
3942
self._refcount = 0
4043
self._lock = asyncio.Lock()
41-
self._cap = None
42-
self._frame = None
44+
self._cap: Optional[cv2.VideoCapture] = None
45+
self._frame: Optional[np.ndarray] = None
4346
self._running = False
4447
self._reader_task: Optional[asyncio.Task] = None
4548

46-
async def acquire(self):
49+
async def acquire(self) -> None:
4750
async with self._lock:
4851
self._refcount += 1
4952
if self._cap is None:
@@ -52,7 +55,7 @@ async def acquire(self):
5255
self._running = True
5356
self._reader_task = asyncio.create_task(self._read_loop())
5457

55-
async def release(self):
58+
async def release(self) -> None:
5659
async with self._lock:
5760
self._refcount -= 1
5861
if self._refcount <= 0:
@@ -68,7 +71,7 @@ async def release(self):
6871
self._reader_task = None
6972
self._refcount = 0
7073

71-
async def _read_loop(self):
74+
async def _read_loop(self) -> None:
7275
loop = asyncio.get_running_loop()
7376
try:
7477
while self._running and self._cap:
@@ -80,11 +83,13 @@ async def _read_loop(self):
8083
except asyncio.CancelledError:
8184
pass
8285

83-
def latest(self):
86+
def latest(self) -> Optional[np.ndarray]:
8487
return self._frame
8588

89+
8690
_shared_cam = _SharedCamera()
8791

92+
8893
def _open_camera(idx: int) -> cv2.VideoCapture:
8994
"""Try platform-appropriate backends before giving up."""
9095
backends: List[int] = []
@@ -97,7 +102,11 @@ def _open_camera(idx: int) -> cv2.VideoCapture:
97102

98103
last_error: Optional[str] = None
99104
for backend in backends:
100-
cap = cv2.VideoCapture(idx, backend) if backend != cv2.CAP_ANY else cv2.VideoCapture(idx)
105+
cap = (
106+
cv2.VideoCapture(idx, backend)
107+
if backend != cv2.CAP_ANY
108+
else cv2.VideoCapture(idx)
109+
)
101110
if cap.isOpened():
102111
return cap
103112
cap.release()
@@ -109,16 +118,17 @@ def _open_camera(idx: int) -> cv2.VideoCapture:
109118
msg += ". Try CAMERA_INDEX=1 or ensure camera permissions are granted."
110119
raise RuntimeError(msg)
111120

121+
112122
# -----------------------------
113123
# Media track
114124
# -----------------------------
115125
class CameraVideoTrack(VideoStreamTrack):
116126
kind = "video"
117127

118-
def __init__(self):
128+
def __init__(self) -> None:
119129
super().__init__()
120130

121-
async def recv(self):
131+
async def recv(self) -> VideoFrame:
122132
pts, time_base = await self.next_timestamp()
123133

124134
frame = None
@@ -134,15 +144,13 @@ async def recv(self):
134144
else:
135145
await asyncio.sleep(0.005)
136146

137-
from av import VideoFrame
138-
import cv2 as _cv2
139-
140-
rgb = _cv2.cvtColor(frame, _cv2.COLOR_BGR2RGB)
147+
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB).astype(np.uint8)
141148
video_frame = VideoFrame.from_ndarray(rgb, format="rgb24")
142149
video_frame.pts = pts
143150
video_frame.time_base = time_base
144151
return video_frame
145152

153+
146154
# -----------------------------
147155
# FastAPI app
148156
# -----------------------------
@@ -157,30 +165,37 @@ async def recv(self):
157165
allow_headers=["*"],
158166
)
159167

168+
160169
class SDPModel(BaseModel):
161170
sdp: str
162171
type: str # "offer"
163172

173+
164174
pcs: List[RTCPeerConnection] = []
165175

176+
166177
@app.get("/health")
167-
def health():
178+
def health() -> dict[str, str]:
168179
return {"status": "ok"}
169180

181+
170182
# Explicit OPTIONS handlers to avoid 405 on preflight in some setups
171183
@app.options("/offer")
172184
@app.options("/offer/")
173-
def options_offer():
185+
def options_offer() -> Response:
174186
return Response(status_code=204)
175187

188+
176189
# Accept both /offer and /offer/
177190
@app.post("/offer")
178191
@app.post("/offer/")
179-
async def offer(sdp: SDPModel):
192+
async def offer(sdp: SDPModel) -> dict[str, str]:
180193
if sdp.type != "offer":
181194
raise HTTPException(400, "type must be 'offer'")
182195

183-
cfg = RTCConfiguration(iceServers=[RTCIceServer(urls=["stun:stun.l.google.com:19302"])])
196+
cfg = RTCConfiguration(
197+
iceServers=[RTCIceServer(urls=["stun:stun.l.google.com:19302"])]
198+
)
184199
pc = RTCPeerConnection(configuration=cfg)
185200
pcs.append(pc)
186201

@@ -201,12 +216,12 @@ async def offer(sdp: SDPModel):
201216
ice_ready.set_result(True)
202217

203218
@pc.on("icegatheringstatechange")
204-
def on_ice_gathering_state_change():
219+
def on_ice_gathering_state_change() -> None:
205220
if pc.iceGatheringState == "complete" and not ice_ready.done():
206221
ice_ready.set_result(True)
207222

208223
@pc.on("iceconnectionstatechange")
209-
async def on_ice_state_change():
224+
async def on_ice_state_change() -> None:
210225
if pc.iceConnectionState in ("failed", "closed", "disconnected"):
211226
await _cleanup_pc(pc)
212227

@@ -220,11 +235,13 @@ async def on_ice_state_change():
220235

221236
return {"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
222237

238+
223239
@app.on_event("shutdown")
224-
async def on_shutdown():
240+
async def on_shutdown() -> None:
225241
await asyncio.gather(*[_cleanup_pc(pc) for pc in list(pcs)], return_exceptions=True)
226242

227-
async def _cleanup_pc(pc: RTCPeerConnection):
243+
244+
async def _cleanup_pc(pc: RTCPeerConnection) -> None:
228245
if pc in pcs:
229246
pcs.remove(pc)
230247
with contextlib.suppress(Exception):
@@ -234,6 +251,6 @@ async def _cleanup_pc(pc: RTCPeerConnection):
234251
await _shared_cam.release()
235252

236253

237-
def _read_frame(cap: cv2.VideoCapture):
254+
def _read_frame(cap: cv2.VideoCapture) -> Tuple[bool, Optional[np.ndarray]]:
238255
"""Run in a thread to grab frames without blocking asyncio loop."""
239-
return cap.read()
256+
return cap.read()

src/frontend/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!doctype html>
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
@@ -10,4 +10,3 @@
1010
<script type="module" src="/src/main.tsx"></script>
1111
</body>
1212
</html>
13-

src/frontend/src/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,3 @@ function App() {
2525
}
2626

2727
export default App;
28-

src/frontend/src/main.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
88
<App />
99
</React.StrictMode>
1010
);
11-

src/frontend/tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,3 @@
2222
},
2323
"include": ["src"]
2424
}
25-

src/frontend/vite.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@ export default defineConfig({
77
port: 3000,
88
},
99
});
10-

0 commit comments

Comments
 (0)