diff options
Diffstat (limited to 'webserver.py')
| -rw-r--r-- | webserver.py | 142 | 
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 | 
