├── LICENSE ├── README.md └── src ├── arduino └── esp32cam_ws_stream │ └── esp32cam_ws_stream.ino └── python_backend ├── templates └── index.html └── websockets_stream.py /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esp32cam-websockets-stream 2 | ESP32CAM websockets stream to a python backend for further image processing. 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/python_backend/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | ESP32CAM Video Surveillance 4 | 5 | 6 |

WebSocket Buttons

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 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 | --------------------------------------------------------------------------------