├── style.css ├── .gitignore ├── requirements.txt ├── index.html ├── script.js ├── README.md └── server.py /style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv-python==4.0.0.21 2 | websockets==7.0 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Proof of concept 5 | 6 | 7 | 8 | 9 |
10 | Connection failed. Somebody may be using the socket. 11 |
12 |
13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | openSocket = () => { 2 | let uri = "ws://" + window.location.hostname + ":8585"; 3 | socket = new WebSocket(uri); 4 | let msg = document.getElementById("msg"); 5 | socket.addEventListener('open', (e) => { 6 | document.getElementById("status").innerHTML = "Opened"; 7 | }); 8 | socket.addEventListener('message', (e) => { 9 | let ctx = msg.getContext("2d"); 10 | let image = new Image(); 11 | image.src = URL.createObjectURL(e.data); 12 | image.addEventListener("load", (e) => { 13 | ctx.drawImage(image, 0, 0, msg.width, msg.height); 14 | }); 15 | }); 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livestreaming webcam through WebSockets 2 | 3 | This is a proof-of-concept demo of using WebSockets to stream a video camera feed through WebSockets, so any browser can access it. 4 | 5 | At the moment, it is very work-in-progress. The stream is coming in as the raw JPEG binary, which is then read as a blob that's loaded into an `Image()`, which is then drawn onto an HTML5 canvas. 6 | 7 | The latency is about ~0.1s, but with relatively high CPU usage (NEW! Down from ~50% to ~20% CPU usage) 8 | 9 | To demo this, make sure you have `opencv-python` and `websockets` installed, and you have Python 3.6+. 10 | 11 | # Demo 12 | 13 | 1. Ensure you have the prerequisites, try `pip install -r requirements.txt` 14 | 2. Run `python server.py` 15 | 3. Visit `localhost:8000` to view the stream. A corresponding window will also be loaded with the current time. 16 | 17 | # Running this in a Raspberry Pi 18 | 19 | This currently works pretty well; I'm noticing ~0.4s latency between frames, but it runs at 30fps otherwise. 20 | 21 | To try it out, make sure you have the `v4l2` drivers installed. Then, enable it with `sudo modprobe bcm2835-v4l2`. You can add it at boot by adding `bcm2835-v4l2` to `/etc/modules`. -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import http.server as http 2 | import asyncio 3 | import websockets 4 | import socketserver 5 | import multiprocessing 6 | import cv2 7 | import sys 8 | from datetime import datetime as dt 9 | 10 | # Keep track of our processes 11 | PROCESSES = [] 12 | 13 | def log(message): 14 | print("[LOG] " + str(dt.now()) + " - " + message) 15 | 16 | def camera(man): 17 | log("Starting camera") 18 | vc = cv2.VideoCapture(0) 19 | 20 | if vc.isOpened(): 21 | r, f = vc.read() 22 | else: 23 | r = False 24 | 25 | while r: 26 | cv2.waitKey(20) 27 | r, f = vc.read() 28 | f = cv2.resize(f, (640, 480)) 29 | encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 65] 30 | man[0] = cv2.imencode('.jpg', f, encode_param)[1] 31 | 32 | # HTTP server handler 33 | def server(): 34 | server_address = ('0.0.0.0', 8000) 35 | if sys.version_info[1] < 7: 36 | class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.HTTPServer): 37 | pass 38 | httpd = ThreadingHTTPServer(server_address, http.SimpleHTTPRequestHandler) 39 | else: 40 | httpd = http.ThreadingHTTPServer(server_address, http.SimpleHTTPRequestHandler) 41 | log("Server started") 42 | httpd.serve_forever() 43 | 44 | def socket(man): 45 | # Will handle our websocket connections 46 | async def handler(websocket, path): 47 | log("Socket opened") 48 | try: 49 | while True: 50 | await asyncio.sleep(0.033) # 30 fps 51 | await websocket.send(man[0].tobytes()) 52 | except websockets.exceptions.ConnectionClosed: 53 | log("Socket closed") 54 | 55 | log("Starting socket handler") 56 | # Create the awaitable object 57 | start_server = websockets.serve(ws_handler=handler, host='0.0.0.0', port=8585) 58 | # Start the server, add it to the event loop 59 | asyncio.get_event_loop().run_until_complete(start_server) 60 | # Registered our websocket connection handler, thus run event loop forever 61 | asyncio.get_event_loop().run_forever() 62 | 63 | 64 | def main(): 65 | # queue = multiprocessing.Queue() 66 | manager = multiprocessing.Manager() 67 | lst = manager.list() 68 | lst.append(None) 69 | # Host the page, creating the server 70 | http_server = multiprocessing.Process(target=server) 71 | # Set up our websocket handler 72 | socket_handler = multiprocessing.Process(target=socket, args=(lst,)) 73 | # Set up our camera 74 | camera_handler = multiprocessing.Process(target=camera, args=(lst,)) 75 | # Add 'em to our list 76 | PROCESSES.append(camera_handler) 77 | PROCESSES.append(http_server) 78 | PROCESSES.append(socket_handler) 79 | for p in PROCESSES: 80 | p.start() 81 | # Wait forever 82 | while True: 83 | pass 84 | 85 | if __name__ == '__main__': 86 | try: 87 | main() 88 | except KeyboardInterrupt: 89 | for p in PROCESSES: 90 | p.terminate() --------------------------------------------------------------------------------