aboutsummaryrefslogtreecommitdiff
path: root/webserver.py
diff options
context:
space:
mode:
Diffstat (limited to 'webserver.py')
-rw-r--r--webserver.py142
1 files changed, 142 insertions, 0 deletions
diff --git a/webserver.py b/webserver.py
new file mode 100644
index 0000000..d1c0a1e
--- /dev/null
+++ b/webserver.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+
+"""
+Module serving video from zmq to a webserver.
+"""
+
+__author__ = "Franoosh Corporation"
+
+
+import os
+from collections import defaultdict
+import json
+import logging
+import asyncio
+from threading import Thread
+import uvicorn
+from fastapi import (
+ FastAPI,
+ Request,
+ HTTPException,
+ WebSocket,
+ templating,
+)
+from fastapi.responses import HTMLResponse
+from fastapi.staticfiles import StaticFiles
+
+from helpers import CustomLoggingFormatter
+import zmq
+
+
+CLIENTS_JSON_FILE = os.path.join(os.getcwd(), 'clients.json')
+LOGFILE = 'webserver.log'
+LOGLEVEL = logging.INFO
+
+HOST = "127.0.0.1"
+ZMQPORT = "9979"
+WSPORT = "8008"
+ZMQ_BACKEND_ADDR = f"tcp://{HOST}:{ZMQPORT}"
+WS_BACKEND_ADDR = f"tcp://{HOST}:{WSPORT}"
+
+log_formatter = CustomLoggingFormatter()
+handler = logging.FileHandler(LOGFILE, encoding='utf-8', mode='a')
+handler.setFormatter(log_formatter)
+logging.root.addHandler(handler)
+logging.root.setLevel(LOGLEVEL)
+logger = logging.getLogger(__name__)
+logging.basicConfig(
+ filename=LOGFILE,
+ datefmt='%Y-%m-%d %I:%M:%S',
+ level=LOGLEVEL,
+)
+
+
+
+app = FastAPI()
+app.mount("/static", StaticFiles(directory="static"), name="static")
+templates = templating.Jinja2Templates(directory='templates')
+
+# Track websocket connections by (client_id, camera_id)
+ws_connections = defaultdict(dict) # ws_connections[client_id][camera_id] = websocket
+
+# Set up a single ZMQ SUB socket for all websocket connections
+zmq_context = zmq.Context()
+zmq_socket = zmq_context.socket(zmq.SUB)
+zmq_socket.bind(WS_BACKEND_ADDR)
+zmq_socket.setsockopt(zmq.SUBSCRIBE, b"") # Subscribe to all topics
+poller = zmq.Poller()
+poller.register(zmq_socket, zmq.POLLIN)
+
+def load_clients():
+ try:
+ with open(CLIENTS_JSON_FILE) as f:
+ clients_dict = json.load(f)
+ except FileNotFoundError:
+ clients_dict = {}
+ return clients_dict
+
+@app.get("/")
+async def main_route(request: Request):
+ logger.error("DEBUG: main route visited")
+ clients = load_clients()
+ return templates.TemplateResponse(
+ "main.html",
+ {
+ "request": request,
+ "clients": clients,
+ }
+ )
+
+@app.get("/clients/{client_id}", response_class=HTMLResponse)
+async def client_route(request: Request, client_id: str):
+ """Serve client page."""
+ clients_dict = load_clients()
+ logger.debug("Checking client_id: '%s' in clients_dict: %r.", client_id, clients_dict)
+ if not client_id in clients_dict:
+ return HTTPException(status_code=404, detail="No such client ID.")
+ return templates.TemplateResponse(
+ "client.html",
+ {
+ "request": request,
+ "client_id": client_id,
+ "camera_ids": clients_dict[client_id],
+ },
+ )
+
+
+@app.websocket("/ws/{client_id}/{camera_id}")
+async def camera_route(websocket: WebSocket, client_id: str, camera_id: str):
+ """Serve a particular camera page."""
+ logger.info("Accepting websocket connection for '/ws/%s/%s'.", client_id, camera_id)
+ await websocket.accept()
+ if client_id not in ws_connections:
+ ws_connections[client_id] = {}
+ ws_connections[client_id][camera_id] = websocket
+ try:
+ while True:
+ # Wait for a frame for this client/camera
+ sockets = dict(poller.poll(1000))
+ if zmq_socket in sockets:
+ msg = zmq_socket.recv_multipart()
+ if len(msg) == 3:
+ recv_client_id, recv_camera_id, content = msg
+ recv_client_id = recv_client_id.decode("utf-8")
+ recv_camera_id = recv_camera_id.decode("utf-8")
+ # Only send to the websocket for this client/camera
+ if recv_client_id == client_id and recv_camera_id == camera_id:
+ await websocket.send_bytes(content)
+ except Exception as exc:
+ logger.warning("Connection closed: %r", exc)
+ finally:
+ if client_id in ws_connections and camera_id in ws_connections[client_id]:
+ del ws_connections[client_id][camera_id]
+ await websocket.close()
+
+
+if __name__ == "__main__":
+ uvicorn.run(
+ app,
+ port=8007,
+ host='127.0.0.1',
+ log_level='info',
+ ) \ No newline at end of file