├── README.md
├── LICENSE
└── src
├── python_backend
├── templates
│ └── index.html
└── websockets_stream.py
└── arduino
└── esp32cam_ws_stream
└── esp32cam_ws_stream.ino
/README.md:
--------------------------------------------------------------------------------
1 | # esp32cam-websockets-stream
2 | ESP32CAM websockets stream to a python backend for further image processing.
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 weimeng soh
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/python_backend/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ESP32CAM Video Surveillance
4 |
5 |
6 | WebSocket Buttons
7 | Up
8 | Down
9 | Left
10 | Right
11 |
12 |
13 |
14 |
15 |
16 | {% for deviceId in deviceIds %}
17 | {{ deviceId }}
18 |
19 | {% end %}
20 |
21 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/python_backend/websockets_stream.py:
--------------------------------------------------------------------------------
1 | import tornado.httpserver
2 | import tornado.websocket
3 | import tornado.concurrent
4 | import tornado.ioloop
5 | import tornado.web
6 | import tornado.gen
7 | import threading
8 | import asyncio
9 | import socket
10 | import numpy as np
11 | import imutils
12 | import copy
13 | import time
14 | import cv2
15 | import os
16 |
17 | bytes = b''
18 |
19 | lock = threading.Lock()
20 | connectedDevices = set()
21 |
22 |
23 | class WSHandler(tornado.websocket.WebSocketHandler):
24 | def __init__(self, *args, **kwargs):
25 | super(WSHandler, self).__init__(*args, **kwargs)
26 | self.outputFrame = None
27 | self.frame = None
28 | self.id = None
29 | self.executor = tornado.concurrent.futures.ThreadPoolExecutor(max_workers=4)
30 | # self.stopEvent = threading.Event()
31 |
32 | def process_frames(self):
33 | if self.frame is None:
34 | return
35 | frame = imutils.rotate_bound(self.frame.copy(), 90)
36 | (flag, encodedImage) = cv2.imencode(".jpg", frame)
37 |
38 | # ensure the frame was successfully encoded
39 | if not flag:
40 | return
41 | self.outputFrame = encodedImage.tobytes()
42 |
43 | def open(self):
44 | print('new connection')
45 | connectedDevices.add(self)
46 | # self.t = threading.Thread(target=self.process_frames)
47 | # self.t.daemon = True
48 | # self.t.start()
49 |
50 | def on_message(self, message):
51 | if self.id is None:
52 | self.id = message
53 | else:
54 | self.frame = cv2.imdecode(np.frombuffer(
55 | message, dtype=np.uint8), cv2.IMREAD_COLOR)
56 | # self.process_frames()
57 | tornado.ioloop.IOLoop.current().run_in_executor(self.executor, self.process_frames)
58 |
59 | def on_close(self):
60 | print('connection closed')
61 | # self.stopEvent.set()
62 | connectedDevices.remove(self)
63 |
64 | def check_origin(self, origin):
65 | return True
66 |
67 |
68 | class StreamHandler(tornado.web.RequestHandler):
69 | @tornado.gen.coroutine
70 | def get(self, slug):
71 | self.set_header(
72 | 'Cache-Control', 'no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0')
73 | self.set_header('Pragma', 'no-cache')
74 | self.set_header(
75 | 'Content-Type', 'multipart/x-mixed-replace;boundary=--jpgboundary')
76 | self.set_header('Connection', 'close')
77 |
78 | my_boundary = "--jpgboundary"
79 | client = None
80 | for c in connectedDevices:
81 | if c.id == slug:
82 | print(slug)
83 | client = c
84 | break
85 | while client is not None:
86 | jpgData = client.outputFrame
87 | if jpgData is None:
88 | print("empty frame")
89 | continue
90 | self.write(my_boundary)
91 | self.write("Content-type: image/jpeg\r\n")
92 | self.write("Content-length: %s\r\n\r\n" % len(jpgData))
93 | self.write(jpgData)
94 | yield self.flush()
95 |
96 | class ButtonHandler(tornado.web.RequestHandler):
97 | def post(self):
98 | data = self.get_argument("data")
99 | for client in connectedDevices:
100 | client.write_message(data)
101 |
102 | def get(self):
103 | self.write("This is a POST-only endpoint.")
104 |
105 |
106 | class TemplateHandler(tornado.web.RequestHandler):
107 | def get(self):
108 | deviceIds = [d.id for d in connectedDevices]
109 | self.render(os.path.sep.join(
110 | [os.path.dirname(__file__), "templates", "index.html"]), url="http://localhost:3000/video_feed/", deviceIds=deviceIds)
111 |
112 |
113 | application = tornado.web.Application([
114 | (r'/video_feed/([^/]+)', StreamHandler),
115 | (r'/ws', WSHandler),
116 | (r'/button', ButtonHandler),
117 | (r'/', TemplateHandler),
118 | ])
119 |
120 |
121 | if __name__ == "__main__":
122 | http_server = tornado.httpserver.HTTPServer(application)
123 | http_server.listen(3000)
124 | myIP = socket.gethostbyname(socket.gethostname())
125 | print('*** Websocket Server Started at %s***' % myIP)
126 | tornado.ioloop.IOLoop.current().start()
127 |
--------------------------------------------------------------------------------
/src/arduino/esp32cam_ws_stream/esp32cam_ws_stream.ino:
--------------------------------------------------------------------------------
1 | #include "esp_camera.h"
2 | #include
3 | #include
4 | #include "esp_timer.h"
5 | #include "img_converters.h"
6 | #include "fb_gfx.h"
7 | #include "soc/soc.h" //disable brownout problems
8 | #include "soc/rtc_cntl_reg.h" //disable brownout problems
9 | #include "driver/gpio.h"
10 |
11 |
12 | #define PWDN_GPIO_NUM 32
13 | #define RESET_GPIO_NUM -1
14 | #define XCLK_GPIO_NUM 0
15 | #define SIOD_GPIO_NUM 26
16 | #define SIOC_GPIO_NUM 27
17 |
18 | #define Y9_GPIO_NUM 35
19 | #define Y8_GPIO_NUM 34
20 | #define Y7_GPIO_NUM 39
21 | #define Y6_GPIO_NUM 36
22 | #define Y5_GPIO_NUM 21
23 | #define Y4_GPIO_NUM 19
24 | #define Y3_GPIO_NUM 18
25 | #define Y2_GPIO_NUM 5
26 | #define VSYNC_GPIO_NUM 25
27 | #define HREF_GPIO_NUM 23
28 | #define PCLK_GPIO_NUM 22
29 |
30 |
31 | char * url = "websockets url";
32 |
33 | camera_fb_t * fb = NULL;
34 | size_t _jpg_buf_len = 0;
35 | uint8_t * _jpg_buf = NULL;
36 | uint8_t state = 0;
37 |
38 | using namespace websockets;
39 | WebsocketsClient client;
40 |
41 | ///////////////////////////////////CALLBACK FUNCTIONS///////////////////////////////////
42 | void onMessageCallback(WebsocketsMessage message) {
43 | Serial.print("Got Message: ");
44 | Serial.println(message.data());
45 | }
46 |
47 | ///////////////////////////////////INITIALIZE FUNCTIONS///////////////////////////////////
48 | esp_err_t init_camera() {
49 | camera_config_t config;
50 | config.ledc_channel = LEDC_CHANNEL_0;
51 | config.ledc_timer = LEDC_TIMER_0;
52 | config.pin_d0 = Y2_GPIO_NUM;
53 | config.pin_d1 = Y3_GPIO_NUM;
54 | config.pin_d2 = Y4_GPIO_NUM;
55 | config.pin_d3 = Y5_GPIO_NUM;
56 | config.pin_d4 = Y6_GPIO_NUM;
57 | config.pin_d5 = Y7_GPIO_NUM;
58 | config.pin_d6 = Y8_GPIO_NUM;
59 | config.pin_d7 = Y9_GPIO_NUM;
60 | config.pin_xclk = XCLK_GPIO_NUM;
61 | config.pin_pclk = PCLK_GPIO_NUM;
62 | config.pin_vsync = VSYNC_GPIO_NUM;
63 | config.pin_href = HREF_GPIO_NUM;
64 | config.pin_sscb_sda = SIOD_GPIO_NUM;
65 | config.pin_sscb_scl = SIOC_GPIO_NUM;
66 | config.pin_pwdn = PWDN_GPIO_NUM;
67 | config.pin_reset = RESET_GPIO_NUM;
68 | config.xclk_freq_hz = 20000000;
69 | config.pixel_format = PIXFORMAT_JPEG;
70 | //init with high specs to pre-allocate larger buffers
71 | if (psramFound()) {
72 | config.frame_size = FRAMESIZE_XGA;
73 | config.jpeg_quality = 12;
74 | config.fb_count = 2;
75 | } else {
76 | config.frame_size = FRAMESIZE_SVGA;
77 | config.jpeg_quality = 12;
78 | config.fb_count = 1;
79 | }
80 | // Camera init
81 | esp_err_t err = esp_camera_init(&config);
82 | if (err != ESP_OK) {
83 | Serial.printf("Camera init failed with error 0x%x", err);
84 | return err;
85 | }
86 | sensor_t * s = esp_camera_sensor_get();
87 | s->set_framesize(s, FRAMESIZE_VGA);
88 | Serial.println("Cam Success init");
89 | return ESP_OK;
90 | };
91 |
92 |
93 | esp_err_t init_wifi() {
94 | WiFi.begin("SSID", "PASSWORD");
95 | Serial.println("Starting Wifi");
96 | while (WiFi.status() != WL_CONNECTED) {
97 | delay(500);
98 | Serial.print(".");
99 | }
100 | Serial.println("");
101 | Serial.println("WiFi connected");
102 | Serial.println("Connecting to websocket");
103 | client.onMessage(onMessageCallback);
104 | bool connected = client.connect(url);
105 | if (!connected) {
106 | Serial.println("Cannot connect to websocket server!");
107 | state = 3;
108 | return ESP_FAIL;
109 | }
110 | if (state == 3) {
111 | return ESP_FAIL;
112 | }
113 |
114 | Serial.println("Websocket Connected!");
115 | client.send("deviceId"); // for verification
116 | return ESP_OK;
117 | };
118 |
119 |
120 | ///////////////////////////////////SETUP///////////////////////////////////
121 | void setup() {
122 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
123 | // disableCore0WDT();
124 |
125 | Serial.begin(115200);
126 | Serial.setDebugOutput(true);
127 | init_camera();
128 | init_wifi();
129 | }
130 |
131 | ///////////////////////////////////MAIN LOOP///////////////////////////////////
132 | void loop() {
133 | if (client.available()) {
134 | camera_fb_t *fb = esp_camera_fb_get();
135 | if (!fb) {
136 | Serial.println("Camera capture failed");
137 | esp_camera_fb_return(fb);
138 | ESP.restart();
139 | }
140 | client.sendBinary((const char*) fb->buf, fb->len);
141 | Serial.println("MJPG sent");
142 | esp_camera_fb_return(fb);
143 | client.poll();
144 | }
145 | }
146 |
--------------------------------------------------------------------------------