├── 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()
--------------------------------------------------------------------------------