├── library.properties ├── library.json ├── examples ├── led │ └── led.ino ├── button │ └── button.ino ├── rtsp_stream │ ├── LinkedListElement.h │ ├── CStreamer.h │ ├── platglue-esp32.h │ ├── CRtspSession.h │ ├── rtsp_stream.ino │ ├── CStreamer.cpp │ └── CRtspSession.cpp ├── capture │ └── capture.ino ├── http_post │ └── http_post.ino └── web_cam │ ├── ap │ └── ap.ino │ ├── sta │ └── sta.ino │ └── eth │ └── eth.ino ├── src ├── M5PoECAM.cpp ├── M5PoECAM.h └── utility │ ├── Camera_Class.h │ ├── Camera_Class.cpp │ ├── Button_Class.cpp │ └── Button_Class.hpp ├── README_cn.md ├── LICENSE └── README.md /library.properties: -------------------------------------------------------------------------------- 1 | name=M5PoECAM 2 | version=1.0.1 3 | author=M5Stack 4 | maintainer=M5Stack 5 | sentence=Library for M5Stack PoE-CAM development kit 6 | paragraph=See more on http://M5Stack.com 7 | category=Device Control 8 | url=https://github.com/m5stack/M5PoECAM.git 9 | architectures=esp32 10 | includes=M5PoECAM.h 11 | depends=M5-Ethernet 12 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "M5PoECAM", 3 | "description": "Library for M5Stack PoE-CAM development kit", 4 | "keywords": "PoE CAM W5500", 5 | "authors": { 6 | "name": "M5Stack", 7 | "url": "http://www.m5stack.com" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/m5stack/M5PoECAM.git" 12 | }, 13 | "dependencies": [ 14 | { 15 | "M5-Ethernet": "*" 16 | } 17 | ], 18 | "version": "1.0.1", 19 | "frameworks": "arduino", 20 | "platforms": "espressif32" 21 | } -------------------------------------------------------------------------------- /examples/led/led.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file led.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM LED Test 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | */ 14 | #include "M5PoECAM.h" 15 | 16 | void setup() { 17 | PoECAM.begin(); 18 | } 19 | 20 | void loop() { 21 | PoECAM.setLed(true); 22 | vTaskDelay(pdMS_TO_TICKS(500)); 23 | PoECAM.setLed(false); 24 | vTaskDelay(pdMS_TO_TICKS(500)); 25 | } 26 | -------------------------------------------------------------------------------- /src/M5PoECAM.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "M5PoECAM.h" 3 | 4 | using namespace m5; 5 | 6 | M5PoECAM PoECAM; 7 | 8 | void M5PoECAM::begin(bool enableLed) { 9 | Serial.begin(115200); 10 | 11 | pinMode(M5_POE_CAM_BTN_A_PIN, INPUT); 12 | _enableLed = enableLed; 13 | if (_enableLed) { 14 | pinMode(M5_POE_CAM_LED_PIN, OUTPUT); 15 | } 16 | } 17 | 18 | void M5PoECAM::setLed(bool status) { 19 | if (_enableLed) { 20 | digitalWrite(M5_POE_CAM_LED_PIN, status); 21 | } 22 | } 23 | 24 | void M5PoECAM::update(void) { 25 | uint32_t ms = millis(); 26 | BtnA.setRawState(ms, !digitalRead(M5_POE_CAM_BTN_A_PIN)); 27 | } -------------------------------------------------------------------------------- /examples/button/button.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file button.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM Button Test 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | */ 14 | #include "M5PoECAM.h" 15 | 16 | void setup() { 17 | PoECAM.begin(); 18 | } 19 | 20 | void loop() { 21 | PoECAM.update(); 22 | if (PoECAM.BtnA.wasPressed()) { 23 | Serial.println("BtnA Pressed"); 24 | } 25 | if (PoECAM.BtnA.wasReleased()) { 26 | Serial.println("BtnA Released"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/M5PoECAM.h: -------------------------------------------------------------------------------- 1 | #ifndef _M5_POE_CAM_H_ 2 | #define _M5_POE_CAM_H_ 3 | 4 | #include "./utility/Camera_Class.h" 5 | #include "./utility/Button_Class.hpp" 6 | #include "esp_camera.h" 7 | #include "Arduino.h" 8 | #include "driver/gpio.h" 9 | 10 | #define M5_POE_CAM_LED_PIN 0 11 | #define M5_POE_CAM_BTN_A_PIN 37 12 | #define M5_POE_CAM_ETH_CS_PIN 4 13 | #define M5_POE_CAM_ETH_MOSI_PIN 13 14 | #define M5_POE_CAM_ETH_MISO_PIN 38 15 | #define M5_POE_CAM_ETH_CLK_PIN 23 16 | 17 | namespace m5 { 18 | 19 | class M5PoECAM { 20 | private: 21 | /* data */ 22 | bool _enableLed; 23 | 24 | public: 25 | void begin(bool enableLed = true); 26 | void setLed(bool status); 27 | Camera_Class Camera; 28 | Button_Class BtnA; 29 | void update(void); 30 | }; 31 | } // namespace m5 32 | extern m5::M5PoECAM PoECAM; 33 | 34 | #endif -------------------------------------------------------------------------------- /src/utility/Camera_Class.h: -------------------------------------------------------------------------------- 1 | #ifndef _M5_TIMER_CAM_CAMERA_H_ 2 | #define _M5_TIMER_CAM_CAMERA_H_ 3 | 4 | #include "esp_camera.h" 5 | #include "Arduino.h" 6 | 7 | #define PWDN_GPIO_NUM -1 8 | #define RESET_GPIO_NUM 15 9 | #define XCLK_GPIO_NUM 27 10 | #define SIOD_GPIO_NUM 14 11 | #define SIOC_GPIO_NUM 12 12 | 13 | #define Y9_GPIO_NUM 19 14 | #define Y8_GPIO_NUM 36 15 | #define Y7_GPIO_NUM 18 16 | #define Y6_GPIO_NUM 39 17 | #define Y5_GPIO_NUM 5 18 | #define Y4_GPIO_NUM 34 19 | #define Y3_GPIO_NUM 35 20 | #define Y2_GPIO_NUM 32 21 | 22 | #define VSYNC_GPIO_NUM 22 23 | #define HREF_GPIO_NUM 26 24 | #define PCLK_GPIO_NUM 21 25 | 26 | class Camera_Class { 27 | private: 28 | public: 29 | camera_fb_t* fb; 30 | sensor_t* sensor; 31 | camera_config_t* config; 32 | bool begin(); 33 | bool get(); 34 | bool free(); 35 | }; 36 | 37 | #endif -------------------------------------------------------------------------------- /README_cn.md: -------------------------------------------------------------------------------- 1 | # PoE_CAM 2 | 3 | 中文 | [English](README_cn.md) 4 | 5 | 6 | M5Core2_P1M5Core2_P1 7 | 8 | * **如果查看 PoECAM 的详细介绍文档,[点击这里](https://docs.m5stack.com/zh_CN/unit/poe_cam)** 9 | 10 | ## 描述 11 | 12 | **PoECAM**是一款集成`PoE`(Power Over Ethernet)功能的一款开源`可编程网络摄像头`。硬件使用`ESP32`控制核心+`W5500嵌入式以太网控制器`+200w像素图像传感器`OV2640`方案。 搭配`8MB PSRAM` + `16MB Flash` 大内存组合。整机体积紧凑, 供电方式灵活, 仅需两步, `连接网线, 即可实现稳定的图像传输`。非常适用于仓储监控, 定时图像采集等应用。 13 | 14 | ## 注意事项: 15 | 16 | 1. PoECAM下载程序需外接ESP32烧录器,你可以[点击此处购买M5官方的ESP32-Downloader套件,内含转接小板,连接会更加的方便](https://shop.m5stack.com/products/esp32-downloader-kit) 17 | 2. PoECAM的出厂固件,在连接交换机后将会自动获取IP,并启动web服务器。通过查看PoECAM的串口输出,可以获取到IP地址与图像流URL,同一局域网下用浏览器的访问该URL,既可以实时预览图像。 18 | 19 | ## 更多信息 20 | 21 | **Arduino IDE 环境搭建**: [点击这里](https://docs.m5stack.com/zh_CN/quick_start/poe_cam/arduino) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 M5Stack 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 | -------------------------------------------------------------------------------- /examples/rtsp_stream/LinkedListElement.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "platglue-esp32.h" 4 | #include 5 | 6 | class LinkedListElement { 7 | public: 8 | LinkedListElement* m_Next; 9 | LinkedListElement* m_Prev; 10 | 11 | LinkedListElement(void) { 12 | m_Next = this; 13 | m_Prev = this; 14 | printf("LinkedListElement (%p)->(%p)->(%p)\n", m_Prev, this, m_Next); 15 | } 16 | 17 | int NotEmpty(void) { 18 | return (m_Next != this); 19 | } 20 | 21 | LinkedListElement(LinkedListElement* linkedList) { 22 | // add to the end of list 23 | m_Prev = linkedList->m_Prev; 24 | linkedList->m_Prev = this; 25 | m_Prev->m_Next = this; 26 | m_Next = linkedList; 27 | printf("LinkedListElement (%p)->(%p)->(%p)\n", m_Prev, this, m_Next); 28 | } 29 | 30 | ~LinkedListElement() { 31 | printf("~LinkedListElement(%p)->(%p)->(%p)\n", m_Prev, this, m_Next); 32 | if (m_Next) m_Next->m_Prev = m_Prev; 33 | if (m_Prev) m_Prev->m_Next = m_Next; 34 | printf("~LinkedListElement after: (%p)->(%p)", m_Prev, m_Prev->m_Next); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /examples/capture/capture.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file capture.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM Take Photo Test 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | */ 14 | #include "M5PoECAM.h" 15 | 16 | void setup() { 17 | PoECAM.begin(); 18 | 19 | if (!PoECAM.Camera.begin()) { 20 | Serial.println("Camera Init Fail"); 21 | return; 22 | } 23 | Serial.println("Camera Init Success"); 24 | 25 | PoECAM.Camera.sensor->set_pixformat(PoECAM.Camera.sensor, 26 | PIXFORMAT_JPEG); 27 | PoECAM.Camera.sensor->set_framesize(PoECAM.Camera.sensor, 28 | FRAMESIZE_QVGA); 29 | 30 | PoECAM.Camera.sensor->set_vflip(PoECAM.Camera.sensor, 1); 31 | PoECAM.Camera.sensor->set_hmirror(PoECAM.Camera.sensor, 0); 32 | } 33 | 34 | void loop() { 35 | if (PoECAM.Camera.get()) { 36 | Serial.printf("pic size: %d\n", PoECAM.Camera.fb->len); 37 | PoECAM.Camera.free(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utility/Camera_Class.cpp: -------------------------------------------------------------------------------- 1 | #include "Camera_Class.h" 2 | 3 | static camera_config_t camera_config = { 4 | .pin_pwdn = PWDN_GPIO_NUM, 5 | .pin_reset = RESET_GPIO_NUM, 6 | .pin_xclk = XCLK_GPIO_NUM, 7 | .pin_sscb_sda = SIOD_GPIO_NUM, 8 | .pin_sscb_scl = SIOC_GPIO_NUM, 9 | .pin_d7 = Y9_GPIO_NUM, 10 | .pin_d6 = Y8_GPIO_NUM, 11 | .pin_d5 = Y7_GPIO_NUM, 12 | .pin_d4 = Y6_GPIO_NUM, 13 | .pin_d3 = Y5_GPIO_NUM, 14 | .pin_d2 = Y4_GPIO_NUM, 15 | .pin_d1 = Y3_GPIO_NUM, 16 | .pin_d0 = Y2_GPIO_NUM, 17 | 18 | .pin_vsync = VSYNC_GPIO_NUM, 19 | .pin_href = HREF_GPIO_NUM, 20 | .pin_pclk = PCLK_GPIO_NUM, 21 | 22 | .xclk_freq_hz = 20000000, 23 | .ledc_timer = LEDC_TIMER_0, 24 | .ledc_channel = LEDC_CHANNEL_0, 25 | 26 | .pixel_format = PIXFORMAT_JPEG, 27 | .frame_size = FRAMESIZE_UXGA, 28 | .jpeg_quality = 16, 29 | .fb_count = 2, 30 | .fb_location = CAMERA_FB_IN_PSRAM, 31 | .grab_mode = CAMERA_GRAB_LATEST, 32 | .sccb_i2c_port = -1, 33 | }; 34 | 35 | bool Camera_Class::begin() { 36 | pinMode(SIOD_GPIO_NUM, PULLUP); 37 | pinMode(SIOC_GPIO_NUM, PULLUP); 38 | esp_err_t err = esp_camera_init(&camera_config); 39 | if (err != ESP_OK) { 40 | return false; 41 | } 42 | sensor = esp_camera_sensor_get(); 43 | return true; 44 | } 45 | 46 | bool Camera_Class::get() { 47 | fb = esp_camera_fb_get(); 48 | if (!fb) { 49 | return false; 50 | } 51 | return true; 52 | } 53 | 54 | bool Camera_Class::free() { 55 | if (fb) { 56 | esp_camera_fb_return(fb); 57 | return true; 58 | } 59 | return false; 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PoE_CAM 2 | 3 | English | [中文](README_cn.md) 4 | 5 | M5Core2_P1M5Core2_P1 6 | 7 | * **For the Detailed documentation of PoECAM, Please [Click here](https://docs.m5stack.com/en/unit/poe_cam)** 8 | 9 | ## Description: 10 | 11 | **PoECAM** is an `open source` Fully `programmable camera` integrating `PoE` (Power Over Ethernet) . This item is `ESP32` based webcam with `W5500` embedded Ethernet controller plus `200w pixel` image sensor `OV2640`. 12 | PoECAM equipped with a combination of `8MB PSRAM` + `16MB Flash` large memory. This teeny tiny with flexible power supply, you can get stable image by connecting the network cable in just 2 steps. It's wonderful for you to monitor warehouse, get auto image acquisition, etc. 13 | 14 | ## Notes: 15 | 16 | 1. The PoECAM download program requires an external ESP32 burner, you can [click here to buy the official M5 ESP32-Downloader kit, which includes a small adapter board, and the connection will be even better Convenient](https://shop.m5stack.com/products/esp32-downloader-kit) 17 | 2. The factory firmware of PoECAM will automatically obtain the IP after connecting to the switch and start the web server. By viewing the serial port output of PoECAM, you can get the IP address and the image stream URL, and you can preview the image in real time by visiting the URL with a browser under the same local area network. 18 | 19 | ## More Information 20 | 21 | **Arduino IDE Development**: [Click Here](https://docs.m5stack.com/en/quick_start/poe_cam/arduino) 22 | -------------------------------------------------------------------------------- /src/utility/Button_Class.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) M5Stack. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #include "Button_Class.hpp" 6 | 7 | namespace m5 { 8 | void Button_Class::setState(std::uint32_t msec, button_state_t state) { 9 | if (_currentState == state_decide_click_count) { 10 | _clickCount = 0; 11 | } 12 | 13 | _lastMsec = msec; 14 | bool flg_timeout = (msec - _lastClicked > _msecHold); 15 | switch (state) { 16 | case state_nochange: 17 | if (flg_timeout && !_press && _clickCount) { 18 | if (_oldPress == 0 && _currentState == state_nochange) { 19 | state = state_decide_click_count; 20 | } else { 21 | _clickCount = 0; 22 | } 23 | } 24 | break; 25 | 26 | case state_clicked: 27 | ++_clickCount; 28 | _lastClicked = msec; 29 | break; 30 | 31 | default: 32 | break; 33 | } 34 | _currentState = state; 35 | } 36 | 37 | void Button_Class::setRawState(std::uint32_t msec, bool press) { 38 | button_state_t state = button_state_t::state_nochange; 39 | bool disable_db = (msec - _lastMsec) > _msecDebounce; 40 | auto oldPress = _press; 41 | _oldPress = oldPress; 42 | if (_raw_press != press) { 43 | _raw_press = press; 44 | _lastRawChange = msec; 45 | } 46 | if (disable_db || msec - _lastRawChange >= _msecDebounce) { 47 | if (press != (0 != oldPress)) { 48 | _lastChange = msec; 49 | } 50 | 51 | if (press) { 52 | std::uint32_t holdPeriod = msec - _lastChange; 53 | _lastHoldPeriod = holdPeriod; 54 | if (!oldPress) { 55 | _press = 1; 56 | } else if (oldPress == 1 && (holdPeriod >= _msecHold)) { 57 | _press = 2; 58 | state = button_state_t::state_hold; 59 | } 60 | } else { 61 | _press = 0; 62 | if (oldPress == 1) { 63 | state = button_state_t::state_clicked; 64 | } 65 | } 66 | } 67 | setState(msec, state); 68 | } 69 | } // namespace m5 -------------------------------------------------------------------------------- /examples/rtsp_stream/CStreamer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // #include "platglue.h" 4 | 5 | #include "platglue-esp32.h" 6 | 7 | #include "LinkedListElement.h" 8 | 9 | typedef unsigned const char *BufPtr; 10 | 11 | class CStreamer { 12 | public: 13 | CStreamer(u_short width, u_short height); 14 | virtual ~CStreamer(); 15 | 16 | void addSession(WiFiClient &aClient); 17 | LinkedListElement *getClientsListHead() { 18 | return &m_Clients; 19 | } 20 | 21 | int anySessions() { 22 | return m_Clients.NotEmpty(); 23 | } 24 | 25 | bool handleRequests(uint32_t readTimeoutMs); 26 | 27 | u_short GetRtpServerPort(); 28 | u_short GetRtcpServerPort(); 29 | 30 | virtual void streamImage( 31 | uint32_t curMsec) = 0; // send a new image to the client 32 | bool InitUdpTransport(void); 33 | void ReleaseUdpTransport(void); 34 | 35 | protected: 36 | void streamFrame(unsigned const char *data, uint32_t dataLen, 37 | uint32_t curMsec); 38 | 39 | private: 40 | int SendRtpPacket(unsigned const char *jpeg, int jpegLen, 41 | int fragmentOffset, BufPtr quant0tbl = NULL, 42 | BufPtr quant1tbl = NULL); // returns new fragmentOffset 43 | // or 0 if finished with frame 44 | 45 | UDPSOCKET m_RtpSocket; // RTP socket for streaming RTP packets to client 46 | UDPSOCKET m_RtcpSocket; // RTCP socket for sending/receiving RTCP packages 47 | 48 | IPPORT m_RtpServerPort; // RTP sender port on server 49 | IPPORT m_RtcpServerPort; // RTCP sender port on server 50 | 51 | u_short m_SequenceNumber; 52 | uint32_t m_Timestamp; 53 | int m_SendIdx; 54 | 55 | LinkedListElement m_Clients; 56 | uint32_t m_prevMsec; 57 | 58 | int m_udpRefCount; 59 | 60 | u_short m_width; // image data info 61 | u_short m_height; 62 | }; 63 | 64 | // When JPEG is stored as a file it is wrapped in a container 65 | // This function fixes up the provided start ptr to point to the 66 | // actual JPEG stream data and returns the number of bytes skipped 67 | // returns true if the file seems to be valid jpeg 68 | // If quant tables can be found they will be stored in qtable0/1 69 | bool decodeJPEGfile(BufPtr *start, uint32_t *len, BufPtr *qtable0, 70 | BufPtr *qtable1); 71 | bool findJPEGheader(BufPtr *start, uint32_t *len, uint8_t marker); 72 | 73 | // Given a jpeg ptr pointing to a pair of length bytes, advance the pointer to 74 | // the next 0xff marker byte 75 | void nextJpegBlock(BufPtr *start); 76 | -------------------------------------------------------------------------------- /examples/http_post/http_post.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file http_post.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM HTTP Post Test 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | * ArduinoHttpClient: https://github.com/arduino-libraries/ArduinoHttpClient 14 | */ 15 | 16 | #include "M5PoECAM.h" 17 | #include 18 | #include 19 | 20 | #define ssid "ssid" 21 | #define password "password" 22 | 23 | #define SERVER "httpbin.org" 24 | 25 | WiFiClient wifi; 26 | HttpClient client = HttpClient(wifi, SERVER); 27 | 28 | void setup() { 29 | PoECAM.begin(); 30 | 31 | if (!PoECAM.Camera.begin()) { 32 | Serial.println("Camera Init Fail"); 33 | return; 34 | } 35 | Serial.println("Camera Init Success"); 36 | 37 | PoECAM.Camera.sensor->set_pixformat(PoECAM.Camera.sensor, PIXFORMAT_JPEG); 38 | PoECAM.Camera.sensor->set_framesize(PoECAM.Camera.sensor, FRAMESIZE_QVGA); 39 | 40 | PoECAM.Camera.sensor->set_vflip(PoECAM.Camera.sensor, 1); 41 | PoECAM.Camera.sensor->set_hmirror(PoECAM.Camera.sensor, 0); 42 | 43 | WiFi.mode(WIFI_STA); 44 | WiFi.begin(ssid, password); 45 | WiFi.setSleep(false); 46 | Serial.println(""); 47 | Serial.print("Connecting to "); 48 | Serial.println(ssid); 49 | // Wait for connection 50 | while (WiFi.status() != WL_CONNECTED) { 51 | delay(500); 52 | Serial.print("."); 53 | } 54 | 55 | Serial.println(""); 56 | 57 | Serial.print("Connected to "); 58 | Serial.println(ssid); 59 | Serial.print("IP address: "); 60 | Serial.println(WiFi.localIP()); 61 | } 62 | 63 | void loop() { 64 | if (PoECAM.Camera.get()) { 65 | Serial.println("making POST request"); 66 | 67 | String contentType = "image/jpeg"; 68 | 69 | // client.post("/post", contentType, postData); 70 | client.post("/post", contentType.c_str(), PoECAM.Camera.fb->len, 71 | PoECAM.Camera.fb->buf); 72 | 73 | // read the status code and body of the response 74 | int statusCode = client.responseStatusCode(); 75 | String response = client.responseBody(); 76 | 77 | Serial.print("Status code: "); 78 | Serial.println(statusCode); 79 | Serial.print("Response: "); 80 | Serial.println(response); 81 | 82 | Serial.println("Wait five seconds"); 83 | PoECAM.Camera.free(); 84 | delay(5000); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/rtsp_stream/platglue-esp32.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | // #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | typedef WiFiClient *SOCKET; 17 | typedef WiFiUDP *UDPSOCKET; 18 | typedef IPAddress 19 | IPADDRESS; // On linux use uint32_t in network byte order (per getpeername) 20 | typedef uint16_t IPPORT; // on linux use network byte order 21 | 22 | #define NULLSOCKET NULL 23 | 24 | inline void closesocket(SOCKET s) { 25 | printf("closing TCP socket\n"); 26 | 27 | if (s) { 28 | s->stop(); 29 | // delete s; TDP WiFiClients are never on the heap in arduino land? 30 | } 31 | } 32 | 33 | #define getRandom() random(65536) 34 | 35 | inline void socketpeeraddr(SOCKET s, IPADDRESS *addr, IPPORT *port) { 36 | *addr = s->remoteIP(); 37 | *port = s->remotePort(); 38 | } 39 | 40 | inline void udpsocketclose(UDPSOCKET s) { 41 | printf("closing UDP socket\n"); 42 | if (s) { 43 | s->stop(); 44 | delete s; 45 | } 46 | } 47 | 48 | inline UDPSOCKET udpsocketcreate(unsigned short portNum) { 49 | UDPSOCKET s = new WiFiUDP(); 50 | 51 | if (!s->begin(portNum)) { 52 | printf("Can't bind port %d\n", portNum); 53 | delete s; 54 | return NULL; 55 | } 56 | 57 | return s; 58 | } 59 | 60 | // TCP sending 61 | inline ssize_t socketsend(SOCKET sockfd, const void *buf, size_t len) { 62 | return sockfd->write((uint8_t *)buf, len); 63 | } 64 | 65 | inline ssize_t udpsocketsend(UDPSOCKET sockfd, const void *buf, size_t len, 66 | IPADDRESS destaddr, IPPORT destport) { 67 | sockfd->beginPacket(destaddr, destport); 68 | sockfd->write((const uint8_t *)buf, len); 69 | if (!sockfd->endPacket()) printf("error sending udp packet\n"); 70 | 71 | return len; 72 | } 73 | 74 | /** 75 | Read from a socket with a timeout. 76 | 77 | Return 0=socket was closed by client, -1=timeout, >0 number of bytes read 78 | */ 79 | inline int socketread(SOCKET sock, char *buf, size_t buflen, int timeoutmsec) { 80 | if (!sock->connected()) { 81 | printf("client has closed the socket\n"); 82 | return 0; 83 | } 84 | 85 | int numAvail = sock->available(); 86 | if (numAvail == 0 && timeoutmsec != 0) { 87 | // sleep and hope for more 88 | delay(timeoutmsec); 89 | numAvail = sock->available(); 90 | } 91 | 92 | if (numAvail == 0) { 93 | // printf("timeout on read\n"); 94 | return -1; 95 | } else { 96 | // int numRead = sock->readBytesUntil('\n', buf, buflen); 97 | int numRead = sock->readBytes(buf, buflen); 98 | // printf("bytes avail %d, read %d: %s", numAvail, numRead, buf); 99 | return numRead; 100 | } 101 | } -------------------------------------------------------------------------------- /examples/rtsp_stream/CRtspSession.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "LinkedListElement.h" 4 | #include "CStreamer.h" 5 | #include "platglue-esp32.h" 6 | 7 | // supported command types 8 | enum RTSP_CMD_TYPES { 9 | RTSP_OPTIONS, 10 | RTSP_DESCRIBE, 11 | RTSP_SETUP, 12 | RTSP_PLAY, 13 | RTSP_TEARDOWN, 14 | RTSP_UNKNOWN 15 | }; 16 | 17 | #define RTSP_BUFFER_SIZE 10000 // for incoming requests, and outgoing responses 18 | #define RTSP_PARAM_STRING_MAX 200 19 | #define MAX_HOSTNAME_LEN 256 20 | 21 | class CRtspSession : public LinkedListElement { 22 | public: 23 | CRtspSession(WiFiClient& aRtspClient, CStreamer* aStreamer); 24 | ~CRtspSession(); 25 | 26 | RTSP_CMD_TYPES Handle_RtspRequest(char const* aRequest, 27 | unsigned aRequestSize); 28 | int GetStreamID(); 29 | 30 | /** 31 | Read from our socket, parsing commands as possible. 32 | 33 | return false if the read timed out 34 | */ 35 | bool handleRequests(uint32_t readTimeoutMs); 36 | 37 | bool m_streaming; 38 | bool m_stopped; 39 | 40 | void InitTransport(u_short aRtpPort, u_short aRtcpPort); 41 | 42 | bool isTcpTransport() { 43 | return m_TcpTransport; 44 | } 45 | SOCKET& getClient() { 46 | return m_RtspClient; 47 | } 48 | 49 | uint16_t getRtpClientPort() { 50 | return m_RtpClientPort; 51 | } 52 | 53 | private: 54 | void Init(); 55 | bool ParseRtspRequest(char const* aRequest, unsigned aRequestSize); 56 | char const* DateHeader(); 57 | 58 | // RTSP request command handlers 59 | void Handle_RtspOPTION(); 60 | void Handle_RtspDESCRIBE(); 61 | void Handle_RtspSETUP(); 62 | void Handle_RtspPLAY(); 63 | 64 | // global session state parameters 65 | int m_RtspSessionID; 66 | WiFiClient m_Client; 67 | SOCKET m_RtspClient; // RTSP socket of that session 68 | int m_StreamID; // number of simulated stream of that session 69 | IPPORT m_ClientRTPPort; // client port for UDP based RTP transport 70 | IPPORT m_ClientRTCPPort; // client port for UDP based RTCP transport 71 | bool m_TcpTransport; // if Tcp based streaming was activated 72 | CStreamer* m_Streamer; // the UDP or TCP streamer of that session 73 | 74 | // parameters of the last received RTSP request 75 | 76 | RTSP_CMD_TYPES 77 | m_RtspCmdType; // command type (if any) of the current request 78 | char m_URLPreSuffix[RTSP_PARAM_STRING_MAX]; // stream name pre suffix 79 | char m_URLSuffix[RTSP_PARAM_STRING_MAX]; // stream name suffix 80 | char m_CSeq[RTSP_PARAM_STRING_MAX]; // RTSP command sequence number 81 | char m_URLHostPort[MAX_HOSTNAME_LEN]; // host:port part of the URL 82 | unsigned m_ContentLength; // SDP string size 83 | 84 | uint16_t 85 | m_RtpClientPort; // RTP receiver port on client (in host byte order!) 86 | uint16_t 87 | m_RtcpClientPort; // RTCP receiver port on client (in host byte order!) 88 | }; 89 | -------------------------------------------------------------------------------- /examples/rtsp_stream/rtsp_stream.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file rtsp_stream.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM RTSP Stream 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | * Micro-RTSP: https://github.com/geeksville/Micro-RTSP 14 | */ 15 | 16 | #include "M5PoECAM.h" 17 | #include 18 | #include "CStreamer.h" 19 | 20 | #define ssid "ssid" 21 | #define password "password" 22 | 23 | class PoECAMRTSP : public CStreamer { 24 | Camera_Class &_camera; 25 | 26 | public: 27 | PoECAMRTSP(Camera_Class &camera, int width, int height) 28 | : CStreamer(width, height), _camera(camera){}; 29 | virtual void streamImage(uint32_t curMsec) { 30 | if (_camera.get()) { 31 | streamFrame(_camera.fb->buf, _camera.fb->len, millis()); 32 | _camera.free(); 33 | } 34 | } 35 | }; 36 | 37 | PoECAMRTSP *streamer; 38 | WiFiServer server(8554); 39 | 40 | void setup() { 41 | PoECAM.begin(); 42 | 43 | if (!PoECAM.Camera.begin()) { 44 | Serial.println("Camera Init Fail"); 45 | return; 46 | } 47 | Serial.println("Camera Init Success"); 48 | 49 | PoECAM.Camera.sensor->set_pixformat(PoECAM.Camera.sensor, PIXFORMAT_JPEG); 50 | PoECAM.Camera.sensor->set_framesize(PoECAM.Camera.sensor, FRAMESIZE_QVGA); 51 | 52 | PoECAM.Camera.sensor->set_vflip(PoECAM.Camera.sensor, 1); 53 | PoECAM.Camera.sensor->set_hmirror(PoECAM.Camera.sensor, 0); 54 | 55 | if (PoECAM.Camera.get()) { 56 | int width = PoECAM.Camera.fb->width; 57 | int height = PoECAM.Camera.fb->height; 58 | streamer = new PoECAMRTSP(PoECAM.Camera, width, height); 59 | PoECAM.Camera.free(); 60 | } 61 | 62 | WiFi.mode(WIFI_STA); 63 | WiFi.begin(ssid, password); 64 | WiFi.setSleep(false); 65 | Serial.println(""); 66 | Serial.print("Connecting to "); 67 | Serial.println(ssid); 68 | // Wait for connection 69 | while (WiFi.status() != WL_CONNECTED) { 70 | delay(500); 71 | Serial.print("."); 72 | } 73 | 74 | Serial.println(""); 75 | 76 | Serial.print("Connected to "); 77 | Serial.println(ssid); 78 | Serial.print("IP address: "); 79 | Serial.println(WiFi.localIP()); 80 | 81 | Serial.print("RTSP URL: rtsp://"); 82 | Serial.print(WiFi.localIP()); 83 | Serial.println(":8554/mjpeg/1"); 84 | 85 | server.begin(); 86 | } 87 | 88 | void loop() { 89 | uint32_t msecPerFrame = 100; 90 | static uint32_t lastimage = millis(); 91 | 92 | // If we have an active client connection, just service that until gone 93 | streamer->handleRequests(0); // we don't use a timeout here, 94 | // instead we send only if we have new enough frames 95 | uint32_t now = millis(); 96 | if (streamer->anySessions()) { 97 | if (now > lastimage + msecPerFrame || 98 | now < lastimage) { // handle clock rollover 99 | streamer->streamImage(now); 100 | lastimage = now; 101 | 102 | // check if we are overrunning our max frame rate 103 | now = millis(); 104 | if (now > lastimage + msecPerFrame) { 105 | Serial.printf("warning exceeding max frame rate of %d ms\n", 106 | now - lastimage); 107 | } 108 | } 109 | } 110 | 111 | WiFiClient rtspClient = server.accept(); 112 | if (rtspClient) { 113 | Serial.print("client: "); 114 | Serial.print(rtspClient.remoteIP()); 115 | Serial.println(); 116 | streamer->addSession(rtspClient); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/utility/Button_Class.hpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) M5Stack. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full 3 | // license information. 4 | 5 | #ifndef __M5_BUTTON_CLASS_H__ 6 | #define __M5_BUTTON_CLASS_H__ 7 | 8 | #include 9 | 10 | namespace m5 { 11 | class Button_Class { 12 | public: 13 | enum button_state_t : std::uint8_t { 14 | state_nochange, 15 | state_clicked, 16 | state_hold, 17 | state_decide_click_count 18 | }; 19 | 20 | /// Returns true when the button is pressed briefly and released. 21 | bool wasClicked(void) const { 22 | return _currentState == state_clicked; 23 | } 24 | 25 | /// Returns true when the button has been held pressed for a while. 26 | bool wasHold(void) const { 27 | return _currentState == state_hold; 28 | } 29 | 30 | /// Returns true when some time has passed since the button was single 31 | /// clicked. 32 | bool wasSingleClicked(void) const { 33 | return _currentState == state_decide_click_count && _clickCount == 1; 34 | } 35 | 36 | /// Returns true when some time has passed since the button was double 37 | /// clicked. 38 | bool wasDoubleClicked(void) const { 39 | return _currentState == state_decide_click_count && _clickCount == 2; 40 | } 41 | 42 | /// Returns true when some time has passed since the button was multiple 43 | /// clicked. 44 | bool wasDecideClickCount(void) const { 45 | return _currentState == state_decide_click_count; 46 | } 47 | 48 | [[deprecated("use wasDecideClickCount()")]] bool wasDeciedClickCount( 49 | void) const { 50 | return wasDecideClickCount(); 51 | } 52 | 53 | std::uint8_t getClickCount(void) const { 54 | return _clickCount; 55 | } 56 | 57 | /// Returns true if the button is currently held pressed. 58 | bool isHolding(void) const { 59 | return _press == 2; 60 | } 61 | bool wasChangePressed(void) const { 62 | return ((bool)_press) != ((bool)_oldPress); 63 | } 64 | 65 | bool isPressed(void) const { 66 | return _press; 67 | } 68 | bool isReleased(void) const { 69 | return !_press; 70 | } 71 | bool wasPressed(void) const { 72 | return !_oldPress && _press; 73 | } 74 | bool wasReleased(void) const { 75 | return _oldPress && !_press; 76 | } 77 | bool wasReleasedAfterHold(void) const { 78 | return !_press && _oldPress == 2; 79 | } 80 | bool wasReleaseFor(std::uint32_t ms) const { 81 | return _oldPress && !_press && _lastHoldPeriod >= ms; 82 | } 83 | 84 | [[deprecated("use wasReleaseFor()")]] bool wasReleasefor( 85 | std::uint32_t ms) const { 86 | return wasReleaseFor(ms); 87 | } 88 | bool pressedFor(std::uint32_t ms) const { 89 | return (_press && _lastMsec - _lastChange >= ms); 90 | } 91 | bool releasedFor(std::uint32_t ms) const { 92 | return (!_press && _lastMsec - _lastChange >= ms); 93 | } 94 | 95 | void setDebounceThresh(std::uint32_t msec) { 96 | _msecDebounce = msec; 97 | } 98 | void setHoldThresh(std::uint32_t msec) { 99 | _msecHold = msec; 100 | } 101 | 102 | void setRawState(std::uint32_t msec, bool press); 103 | void setState(std::uint32_t msec, button_state_t state); 104 | button_state_t getState(void) const { 105 | return _currentState; 106 | } 107 | std::uint32_t lastChange(void) const { 108 | return _lastChange; 109 | } 110 | 111 | std::uint32_t getDebounceThresh(void) const { 112 | return _msecDebounce; 113 | } 114 | std::uint32_t getHoldThresh(void) const { 115 | return _msecHold; 116 | } 117 | 118 | std::uint32_t getUpdateMsec(void) const { 119 | return _lastMsec; 120 | } 121 | 122 | private: 123 | std::uint32_t _lastMsec = 0; 124 | std::uint32_t _lastChange = 0; 125 | std::uint32_t _lastRawChange = 0; 126 | std::uint32_t _lastClicked = 0; 127 | std::uint16_t _msecDebounce = 10; 128 | std::uint16_t _msecHold = 500; 129 | std::uint16_t _lastHoldPeriod = 0; 130 | button_state_t _currentState = 131 | state_nochange; // 0:nochange 1:click 2:hold 132 | bool _raw_press = false; 133 | std::uint8_t _press = 0; // 0:release 1:click 2:holding 134 | std::uint8_t _oldPress = 0; 135 | std::uint8_t _clickCount = 0; 136 | }; 137 | 138 | } // namespace m5 139 | 140 | #endif -------------------------------------------------------------------------------- /examples/web_cam/ap/ap.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ap.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM WEB CAM AP Mode 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | */ 14 | #include "M5PoECAM.h" 15 | #include 16 | 17 | const char* ssid = "PoECAM-AP"; 18 | const char* password = "12345678"; 19 | 20 | WiFiServer server(80); 21 | static void jpegStream(WiFiClient* client); 22 | 23 | void setup() { 24 | PoECAM.begin(); 25 | 26 | if (!PoECAM.Camera.begin()) { 27 | Serial.println("Camera Init Fail"); 28 | return; 29 | } 30 | Serial.println("Camera Init Success"); 31 | 32 | PoECAM.Camera.sensor->set_pixformat(PoECAM.Camera.sensor, PIXFORMAT_JPEG); 33 | PoECAM.Camera.sensor->set_framesize(PoECAM.Camera.sensor, FRAMESIZE_SVGA); 34 | PoECAM.Camera.sensor->set_vflip(PoECAM.Camera.sensor, 1); 35 | PoECAM.Camera.sensor->set_hmirror(PoECAM.Camera.sensor, 0); 36 | 37 | if (!WiFi.softAP(ssid, password)) { 38 | log_e("Soft AP creation failed."); 39 | while (1) 40 | ; 41 | } 42 | 43 | Serial.println("AP SSID:"); 44 | Serial.println(ssid); 45 | Serial.println("AP PASSWORD:"); 46 | Serial.println(password); 47 | 48 | IPAddress IP = WiFi.softAPIP(); 49 | Serial.print("AP IP address: "); 50 | Serial.println(IP); 51 | server.begin(); 52 | Serial.print("Server At: "); 53 | Serial.print(IP); 54 | } 55 | 56 | void loop() { 57 | WiFiClient client = server.available(); // listen for incoming clients 58 | 59 | if (client) { // if you get a client, 60 | Serial.println("New Client."); // print a message out the serial port 61 | while (client.connected()) { // loop while the client's connected 62 | if (client.available()) { // if there's bytes to read from the 63 | jpegStream(&client); 64 | } 65 | } 66 | // close the connection: 67 | client.stop(); 68 | Serial.println("Client Disconnected."); 69 | } 70 | } 71 | 72 | // used to image stream 73 | #define PART_BOUNDARY "123456789000000000000987654321" 74 | static const char* _STREAM_CONTENT_TYPE = 75 | "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; 76 | static const char* _STREAM_BOUNDARY = "--" PART_BOUNDARY "\r\n"; 77 | static const char* _STREAM_PART = 78 | "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; 79 | 80 | static void jpegStream(WiFiClient* client) { 81 | Serial.println("Image stream start"); 82 | client->println("HTTP/1.1 200 OK"); 83 | client->printf("Content-Type: %s\r\n", _STREAM_CONTENT_TYPE); 84 | client->println("Content-Disposition: inline; filename=capture.jpg"); 85 | client->println("Access-Control-Allow-Origin: *"); 86 | client->println(); 87 | static int64_t last_frame = 0; 88 | if (!last_frame) { 89 | last_frame = esp_timer_get_time(); 90 | } 91 | 92 | for (;;) { 93 | if (PoECAM.Camera.get()) { 94 | PoECAM.setLed(true); 95 | Serial.printf("pic size: %d\n", PoECAM.Camera.fb->len); 96 | 97 | client->print(_STREAM_BOUNDARY); 98 | client->printf(_STREAM_PART, PoECAM.Camera.fb->len); 99 | int32_t to_sends = PoECAM.Camera.fb->len; 100 | int32_t now_sends = 0; 101 | uint8_t* out_buf = PoECAM.Camera.fb->buf; 102 | uint32_t packet_len = 2048; 103 | while (to_sends > 0) { 104 | now_sends = to_sends > packet_len ? packet_len : to_sends; 105 | if (client->write(out_buf, now_sends) == 0) { 106 | goto client_exit; 107 | } 108 | out_buf += now_sends; 109 | to_sends -= packet_len; 110 | } 111 | 112 | int64_t fr_end = esp_timer_get_time(); 113 | int64_t frame_time = fr_end - last_frame; 114 | last_frame = fr_end; 115 | frame_time /= 1000; 116 | Serial.printf("MJPG: %luKB %lums (%.1ffps)\r\n", 117 | (long unsigned int)(PoECAM.Camera.fb->len / 1024), 118 | (long unsigned int)frame_time, 119 | 1000.0 / (long unsigned int)frame_time); 120 | 121 | PoECAM.Camera.free(); 122 | PoECAM.setLed(false); 123 | } 124 | } 125 | 126 | client_exit: 127 | PoECAM.Camera.free(); 128 | PoECAM.setLed(false); 129 | client->stop(); 130 | Serial.printf("Image stream end\r\n"); 131 | } 132 | -------------------------------------------------------------------------------- /examples/web_cam/sta/sta.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file sta.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM WEB CAM STA Mode 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | */ 14 | #include "M5PoECAM.h" 15 | #include 16 | 17 | const char* ssid = "yourssid"; 18 | const char* password = "yourpasswd"; 19 | 20 | WiFiServer server(80); 21 | static void jpegStream(WiFiClient* client); 22 | 23 | void setup() { 24 | PoECAM.begin(); 25 | 26 | if (!PoECAM.Camera.begin()) { 27 | Serial.println("Camera Init Fail"); 28 | return; 29 | } 30 | Serial.println("Camera Init Success"); 31 | 32 | PoECAM.Camera.sensor->set_pixformat(PoECAM.Camera.sensor, PIXFORMAT_JPEG); 33 | PoECAM.Camera.sensor->set_framesize(PoECAM.Camera.sensor, FRAMESIZE_SVGA); 34 | PoECAM.Camera.sensor->set_vflip(PoECAM.Camera.sensor, 1); 35 | PoECAM.Camera.sensor->set_hmirror(PoECAM.Camera.sensor, 0); 36 | 37 | WiFi.mode(WIFI_STA); 38 | WiFi.begin(ssid, password); 39 | WiFi.setSleep(false); 40 | Serial.println(""); 41 | Serial.print("Connecting to "); 42 | Serial.println(ssid); 43 | // Wait for connection 44 | while (WiFi.status() != WL_CONNECTED) { 45 | delay(500); 46 | Serial.print("."); 47 | } 48 | 49 | Serial.println(""); 50 | Serial.print("Connected to "); 51 | Serial.println(ssid); 52 | Serial.print("IP address: "); 53 | Serial.println(WiFi.localIP()); 54 | server.begin(); 55 | 56 | Serial.print("Server At: "); 57 | Serial.print(WiFi.localIP()); 58 | } 59 | 60 | void loop() { 61 | WiFiClient client = server.available(); // listen for incoming clients 62 | if (client) { // if you get a client, 63 | while (client.connected()) { // loop while the client's connected 64 | if (client.available()) { // if there's bytes to read from the 65 | jpegStream(&client); 66 | } 67 | } 68 | // close the connection: 69 | client.stop(); 70 | Serial.println("Client Disconnected."); 71 | } 72 | } 73 | 74 | // used to image stream 75 | #define PART_BOUNDARY "123456789000000000000987654321" 76 | static const char* _STREAM_CONTENT_TYPE = 77 | "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; 78 | static const char* _STREAM_BOUNDARY = "--" PART_BOUNDARY "\r\n"; 79 | static const char* _STREAM_PART = 80 | "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; 81 | 82 | static void jpegStream(WiFiClient* client) { 83 | Serial.println("Image stream start"); 84 | client->println("HTTP/1.1 200 OK"); 85 | client->printf("Content-Type: %s\r\n", _STREAM_CONTENT_TYPE); 86 | client->println("Content-Disposition: inline; filename=capture.jpg"); 87 | client->println("Access-Control-Allow-Origin: *"); 88 | client->println(); 89 | static int64_t last_frame = 0; 90 | if (!last_frame) { 91 | last_frame = esp_timer_get_time(); 92 | } 93 | 94 | for (;;) { 95 | if (PoECAM.Camera.get()) { 96 | PoECAM.setLed(true); 97 | Serial.printf("pic size: %d\n", PoECAM.Camera.fb->len); 98 | 99 | client->print(_STREAM_BOUNDARY); 100 | client->printf(_STREAM_PART, PoECAM.Camera.fb->len); 101 | int32_t to_sends = PoECAM.Camera.fb->len; 102 | int32_t now_sends = 0; 103 | uint8_t* out_buf = PoECAM.Camera.fb->buf; 104 | uint32_t packet_len = 2048; 105 | while (to_sends > 0) { 106 | now_sends = to_sends > packet_len ? packet_len : to_sends; 107 | if (client->write(out_buf, now_sends) == 0) { 108 | goto client_exit; 109 | } 110 | out_buf += now_sends; 111 | to_sends -= packet_len; 112 | } 113 | 114 | int64_t fr_end = esp_timer_get_time(); 115 | int64_t frame_time = fr_end - last_frame; 116 | last_frame = fr_end; 117 | frame_time /= 1000; 118 | Serial.printf("MJPG: %luKB %lums (%.1ffps)\r\n", 119 | (long unsigned int)(PoECAM.Camera.fb->len / 1024), 120 | (long unsigned int)frame_time, 121 | 1000.0 / (long unsigned int)frame_time); 122 | 123 | PoECAM.Camera.free(); 124 | PoECAM.setLed(false); 125 | } 126 | } 127 | 128 | client_exit: 129 | PoECAM.Camera.free(); 130 | PoECAM.setLed(false); 131 | client->stop(); 132 | Serial.printf("Image stream end\r\n"); 133 | } 134 | -------------------------------------------------------------------------------- /examples/web_cam/eth/eth.ino: -------------------------------------------------------------------------------- 1 | /** 2 | * @file eth.ino 3 | * @author SeanKwok (shaoxiang@m5stack.com) 4 | * @brief PoECAM WEB CAM Ethenet Mode 5 | * @version 0.1 6 | * @date 2024-01-02 7 | * 8 | * 9 | * @Hardwares: M5PoECAM 10 | * @Platform Version: Arduino M5Stack Board Manager v2.0.9 11 | * @Dependent Library: 12 | * M5PoECAM: https://github.com/m5stack/M5PoECAM 13 | */ 14 | #include "M5PoECAM.h" 15 | #include 16 | #include 17 | 18 | // if you want to config the static IP 19 | // IPAddress ip(192, 168, 2, 88); 20 | // IPAddress myDns(192, 168, 2, 1); 21 | // IPAddress gateway(192, 168, 2, 1); 22 | // IPAddress subnet(255, 255, 255, 0); 23 | 24 | EthernetServer server(80); 25 | static void jpegStream(EthernetClient* client); 26 | 27 | byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED}; 28 | 29 | void setup() { 30 | PoECAM.begin(); 31 | 32 | if (!PoECAM.Camera.begin()) { 33 | Serial.println("Camera Init Fail"); 34 | return; 35 | } 36 | Serial.println("Camera Init Success"); 37 | 38 | PoECAM.Camera.sensor->set_pixformat(PoECAM.Camera.sensor, PIXFORMAT_JPEG); 39 | PoECAM.Camera.sensor->set_framesize(PoECAM.Camera.sensor, FRAMESIZE_SVGA); 40 | PoECAM.Camera.sensor->set_vflip(PoECAM.Camera.sensor, 1); 41 | PoECAM.Camera.sensor->set_hmirror(PoECAM.Camera.sensor, 0); 42 | 43 | SPI.begin(M5_POE_CAM_ETH_CLK_PIN, M5_POE_CAM_ETH_MISO_PIN, 44 | M5_POE_CAM_ETH_MOSI_PIN, -1); 45 | 46 | Ethernet.init(M5_POE_CAM_ETH_CS_PIN); 47 | 48 | // if you want to config the static IP 49 | // Ethernet.begin(mac, ip, myDns, gateway, subnet); 50 | 51 | while (Ethernet.begin(mac) != 1) { 52 | Serial.println("Error getting IP address via DHCP, trying again..."); 53 | delay(2000); 54 | } 55 | 56 | server.begin(); 57 | Serial.print("Server At: "); 58 | Serial.print(Ethernet.localIP()); 59 | } 60 | 61 | void loop() { 62 | EthernetClient client = server.available(); // listen for incoming clients 63 | 64 | if (client) { // if you get a client, 65 | Serial.println("New Client."); // print a message out the serial port 66 | while (client.connected()) { // loop while the client's connected 67 | if (client.available()) { // if there's bytes to read from the 68 | jpegStream(&client); 69 | } 70 | } 71 | // close the connection: 72 | client.stop(); 73 | Serial.println("Client Disconnected."); 74 | } 75 | } 76 | 77 | // used to image stream 78 | #define PART_BOUNDARY "123456789000000000000987654321" 79 | static const char* _STREAM_CONTENT_TYPE = 80 | "multipart/x-mixed-replace;boundary=" PART_BOUNDARY; 81 | static const char* _STREAM_BOUNDARY = "--" PART_BOUNDARY "\r\n"; 82 | static const char* _STREAM_PART = 83 | "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; 84 | 85 | static void jpegStream(EthernetClient* client) { 86 | Serial.println("Image stream start"); 87 | client->println("HTTP/1.1 200 OK"); 88 | client->printf("Content-Type: %s\r\n", _STREAM_CONTENT_TYPE); 89 | client->println("Content-Disposition: inline; filename=capture.jpg"); 90 | client->println("Access-Control-Allow-Origin: *"); 91 | client->println(); 92 | static int64_t last_frame = 0; 93 | if (!last_frame) { 94 | last_frame = esp_timer_get_time(); 95 | } 96 | 97 | for (;;) { 98 | if (PoECAM.Camera.get()) { 99 | PoECAM.setLed(true); 100 | Serial.printf("pic size: %d\n", PoECAM.Camera.fb->len); 101 | 102 | client->print(_STREAM_BOUNDARY); 103 | client->printf(_STREAM_PART, PoECAM.Camera.fb->len); 104 | int32_t to_sends = PoECAM.Camera.fb->len; 105 | int32_t now_sends = 0; 106 | uint8_t* out_buf = PoECAM.Camera.fb->buf; 107 | uint32_t packet_len = 2048; 108 | while (to_sends > 0) { 109 | now_sends = to_sends > packet_len ? packet_len : to_sends; 110 | if (client->write(out_buf, now_sends) == 0) { 111 | goto client_exit; 112 | } 113 | out_buf += now_sends; 114 | to_sends -= packet_len; 115 | } 116 | 117 | int64_t fr_end = esp_timer_get_time(); 118 | int64_t frame_time = fr_end - last_frame; 119 | last_frame = fr_end; 120 | frame_time /= 1000; 121 | Serial.printf("MJPG: %luKB %lums (%.1ffps)\r\n", 122 | (long unsigned int)(PoECAM.Camera.fb->len / 1024), 123 | (long unsigned int)frame_time, 124 | 1000.0 / (long unsigned int)frame_time); 125 | 126 | PoECAM.Camera.free(); 127 | PoECAM.setLed(false); 128 | } 129 | } 130 | 131 | client_exit: 132 | PoECAM.Camera.free(); 133 | PoECAM.setLed(0); 134 | client->stop(); 135 | Serial.printf("Image stream end\r\n"); 136 | } 137 | -------------------------------------------------------------------------------- /examples/rtsp_stream/CStreamer.cpp: -------------------------------------------------------------------------------- 1 | #include "CStreamer.h" 2 | #include "CRtspSession.h" 3 | 4 | #include 5 | 6 | CStreamer::CStreamer(u_short width, u_short height) : m_Clients() { 7 | printf("Creating TSP streamer\n"); 8 | m_RtpServerPort = 0; 9 | m_RtcpServerPort = 0; 10 | 11 | m_SequenceNumber = 0; 12 | m_Timestamp = 0; 13 | m_SendIdx = 0; 14 | 15 | m_RtpSocket = NULLSOCKET; 16 | m_RtcpSocket = NULLSOCKET; 17 | 18 | m_width = width; 19 | m_height = height; 20 | m_prevMsec = 0; 21 | 22 | m_udpRefCount = 0; 23 | }; 24 | 25 | CStreamer::~CStreamer() { 26 | LinkedListElement *element = m_Clients.m_Next; 27 | CRtspSession *session = NULL; 28 | while (element != &m_Clients) { 29 | session = static_cast(element); 30 | element = element->m_Next; 31 | delete session; 32 | } 33 | }; 34 | 35 | void CStreamer::addSession(WiFiClient &aClient) { 36 | // printf("CStreamer::addSession\n"); 37 | new CRtspSession(aClient, this); // our threads RTSP session and state 38 | // we have it stored in m_Clients 39 | } 40 | 41 | int CStreamer::SendRtpPacket(unsigned const char *jpeg, int jpegLen, 42 | int fragmentOffset, BufPtr quant0tbl, 43 | BufPtr quant1tbl) { 44 | // printf("CStreamer::SendRtpPacket offset:%d - begin\n", fragmentOffset); 45 | #define KRtpHeaderSize 12 // size of the RTP header 46 | #define KJpegHeaderSize 8 // size of the special JPEG payload header 47 | 48 | #define MAX_FRAGMENT_SIZE 1100 // FIXME, pick more carefully 49 | int fragmentLen = MAX_FRAGMENT_SIZE; 50 | if (fragmentLen + fragmentOffset > 51 | jpegLen) // Shrink last fragment if needed 52 | fragmentLen = jpegLen - fragmentOffset; 53 | 54 | bool isLastFragment = (fragmentOffset + fragmentLen) == jpegLen; 55 | 56 | if (!m_Clients.NotEmpty()) { 57 | return isLastFragment ? 0 : fragmentOffset; 58 | } 59 | 60 | // Do we have custom quant tables? If so include them per RFC 61 | 62 | bool includeQuantTbl = quant0tbl && quant1tbl && fragmentOffset == 0; 63 | uint8_t q = includeQuantTbl ? 128 : 0x5e; 64 | 65 | static char RtpBuf[2048]; // Note: we assume single threaded, this large 66 | // buf we keep off of the tiny stack 67 | int RtpPacketSize = fragmentLen + KRtpHeaderSize + KJpegHeaderSize + 68 | (includeQuantTbl ? (4 + 64 * 2) : 0); 69 | 70 | memset(RtpBuf, 0x00, sizeof(RtpBuf)); 71 | // Prepare the first 4 byte of the packet. This is the Rtp over Rtsp header 72 | // in case of TCP based transport 73 | RtpBuf[0] = '$'; // magic number 74 | RtpBuf[1] = 0; // number of multiplexed subchannel on RTPS connection - 75 | // here the RTP channel 76 | RtpBuf[2] = (RtpPacketSize & 0x0000FF00) >> 8; 77 | RtpBuf[3] = (RtpPacketSize & 0x000000FF); 78 | // Prepare the 12 byte RTP header 79 | RtpBuf[4] = 0x80; // RTP version 80 | RtpBuf[5] = 81 | 0x1a | 82 | (isLastFragment ? 0x80 : 0x00); // JPEG payload (26) and marker bit 83 | RtpBuf[7] = m_SequenceNumber & 84 | 0x0FF; // each packet is counted with a sequence counter 85 | RtpBuf[6] = m_SequenceNumber >> 8; 86 | RtpBuf[8] = 87 | (m_Timestamp & 0xFF000000) >> 24; // each image gets a timestamp 88 | RtpBuf[9] = (m_Timestamp & 0x00FF0000) >> 16; 89 | RtpBuf[10] = (m_Timestamp & 0x0000FF00) >> 8; 90 | RtpBuf[11] = (m_Timestamp & 0x000000FF); 91 | RtpBuf[12] = 0x13; // 4 byte SSRC (sychronization source identifier) 92 | RtpBuf[13] = 0xf9; // we just an arbitrary number here to keep it simple 93 | RtpBuf[14] = 0x7e; 94 | RtpBuf[15] = 0x67; 95 | 96 | // Prepare the 8 byte payload JPEG header 97 | RtpBuf[16] = 0x00; // type specific 98 | RtpBuf[17] = (fragmentOffset & 0x00FF0000) >> 99 | 16; // 3 byte fragmentation offset for fragmented images 100 | RtpBuf[18] = (fragmentOffset & 0x0000FF00) >> 8; 101 | RtpBuf[19] = (fragmentOffset & 0x000000FF); 102 | 103 | /* These sampling factors indicate that the chrominance components of 104 | type 0 video is downsampled horizontally by 2 (often called 4:2:2) 105 | while the chrominance components of type 1 video are downsampled both 106 | horizontally and vertically by 2 (often called 4:2:0). */ 107 | RtpBuf[20] = 0x00; // type (fixme might be wrong for camera data) 108 | // https://tools.ietf.org/html/rfc2435 109 | RtpBuf[21] = q; // quality scale factor was 0x5e 110 | RtpBuf[22] = m_width / 8; // width / 8 111 | RtpBuf[23] = m_height / 8; // height / 8 112 | 113 | int headerLen = 24; // Inlcuding jpeg header but not qant table header 114 | if (includeQuantTbl) { // we need a quant header - but only in first packet 115 | // of the frame 116 | // printf("inserting quanttbl\n"); 117 | RtpBuf[24] = 0; // MBZ 118 | RtpBuf[25] = 0; // 8 bit precision 119 | RtpBuf[26] = 0; // MSB of lentgh 120 | 121 | int numQantBytes = 64; // Two 64 byte tables 122 | RtpBuf[27] = 2 * numQantBytes; // LSB of length 123 | 124 | headerLen += 4; 125 | 126 | memcpy(RtpBuf + headerLen, quant0tbl, numQantBytes); 127 | headerLen += numQantBytes; 128 | 129 | memcpy(RtpBuf + headerLen, quant1tbl, numQantBytes); 130 | headerLen += numQantBytes; 131 | } 132 | // printf("Sending timestamp %d, seq %d, fragoff %d, fraglen %d, jpegLen 133 | // %d\n", m_Timestamp, m_SequenceNumber, fragmentOffset, fragmentLen, 134 | // jpegLen); 135 | 136 | // append the JPEG scan data to the RTP buffer 137 | memcpy(RtpBuf + headerLen, jpeg + fragmentOffset, fragmentLen); 138 | fragmentOffset += fragmentLen; 139 | 140 | m_SequenceNumber++; // prepare the packet counter for the next packet 141 | 142 | IPADDRESS otherip; 143 | IPPORT otherport; 144 | 145 | // RTP marker bit must be set on last fragment 146 | LinkedListElement *element = m_Clients.m_Next; 147 | CRtspSession *session = NULL; 148 | while (element != &m_Clients) { 149 | session = static_cast(element); 150 | if (session->m_streaming && !session->m_stopped) { 151 | if (session->isTcpTransport()) // RTP over RTSP - we send the 152 | // buffer + 4 byte additional header 153 | socketsend(session->getClient(), RtpBuf, RtpPacketSize + 4); 154 | else // UDP - we send just the buffer by skipping the 4 byte RTP 155 | // over RTSP header 156 | { 157 | socketpeeraddr(session->getClient(), &otherip, &otherport); 158 | udpsocketsend(m_RtpSocket, &RtpBuf[4], RtpPacketSize, otherip, 159 | session->getRtpClientPort()); 160 | } 161 | } 162 | element = element->m_Next; 163 | } 164 | // printf("CStreamer::SendRtpPacket offset:%d - end\n", fragmentOffset); 165 | return isLastFragment ? 0 : fragmentOffset; 166 | }; 167 | 168 | u_short CStreamer::GetRtpServerPort() { 169 | return m_RtpServerPort; 170 | }; 171 | 172 | u_short CStreamer::GetRtcpServerPort() { 173 | return m_RtcpServerPort; 174 | }; 175 | 176 | bool CStreamer::InitUdpTransport(void) { 177 | if (m_udpRefCount != 0) { 178 | ++m_udpRefCount; 179 | return true; 180 | } 181 | 182 | for (u_short P = 6970; P < 0xFFFE; P += 2) { 183 | m_RtpSocket = udpsocketcreate(P); 184 | if (m_RtpSocket) { // Rtp socket was bound successfully. Lets try to 185 | // bind the consecutive Rtsp socket 186 | m_RtcpSocket = udpsocketcreate(P + 1); 187 | if (m_RtcpSocket) { 188 | m_RtpServerPort = P; 189 | m_RtcpServerPort = P + 1; 190 | break; 191 | } else { 192 | udpsocketclose(m_RtpSocket); 193 | udpsocketclose(m_RtcpSocket); 194 | }; 195 | } 196 | }; 197 | ++m_udpRefCount; 198 | return true; 199 | } 200 | 201 | void CStreamer::ReleaseUdpTransport(void) { 202 | --m_udpRefCount; 203 | if (m_udpRefCount == 0) { 204 | m_RtpServerPort = 0; 205 | m_RtcpServerPort = 0; 206 | udpsocketclose(m_RtpSocket); 207 | udpsocketclose(m_RtcpSocket); 208 | 209 | m_RtpSocket = NULLSOCKET; 210 | m_RtcpSocket = NULLSOCKET; 211 | } 212 | } 213 | 214 | /** 215 | Call handleRequests on all sessions 216 | */ 217 | bool CStreamer::handleRequests(uint32_t readTimeoutMs) { 218 | bool retVal = true; 219 | LinkedListElement *element = m_Clients.m_Next; 220 | while (element != &m_Clients) { 221 | CRtspSession *session = static_cast(element); 222 | retVal &= session->handleRequests(readTimeoutMs); 223 | 224 | element = element->m_Next; 225 | 226 | if (session->m_stopped) { 227 | // remove session here, so we wont have to send to it 228 | delete session; 229 | } 230 | } 231 | 232 | return retVal; 233 | } 234 | 235 | void CStreamer::streamFrame(unsigned const char *data, uint32_t dataLen, 236 | uint32_t curMsec) { 237 | if (m_prevMsec == 0) // first frame init our timestamp 238 | m_prevMsec = curMsec; 239 | 240 | // compute deltat (being careful to handle clock rollover with a little lie) 241 | uint32_t deltams = (curMsec >= m_prevMsec) ? curMsec - m_prevMsec : 100; 242 | m_prevMsec = curMsec; 243 | 244 | // locate quant tables if possible 245 | BufPtr qtable0, qtable1; 246 | 247 | if (!decodeJPEGfile(&data, &dataLen, &qtable0, &qtable1)) { 248 | printf("can't decode jpeg data\n"); 249 | return; 250 | } 251 | 252 | int offset = 0; 253 | do { 254 | offset = SendRtpPacket(data, dataLen, offset, qtable0, qtable1); 255 | } while (offset != 0); 256 | 257 | // Increment ONLY after a full frame 258 | uint32_t units = 90000; // Hz per RFC 2435 259 | m_Timestamp += 260 | (units * deltams / 261 | 1000); // fixed timestamp increment for a frame rate of 25fps 262 | 263 | m_SendIdx++; 264 | if (m_SendIdx > 1) m_SendIdx = 0; 265 | }; 266 | 267 | #include 268 | 269 | // search for a particular JPEG marker, moves *start to just after that marker 270 | // This function fixes up the provided start ptr to point to the 271 | // actual JPEG stream data and returns the number of bytes skipped 272 | // APP0 e0 273 | // DQT db 274 | // DQT db 275 | // DHT c4 276 | // DHT c4 277 | // DHT c4 278 | // DHT c4 279 | // SOF0 c0 baseline (not progressive) 3 color 0x01 Y, 0x21 2h1v, 0x00 tbl0 280 | // - 0x02 Cb, 0x11 1h1v, 0x01 tbl1 - 0x03 Cr, 0x11 1h1v, 0x01 tbl1 281 | // therefore 4:2:2, with two separate quant tables (0 and 1) 282 | // SOS da 283 | // EOI d9 (no need to strip data after this RFC says client will discard) 284 | bool findJPEGheader(BufPtr *start, uint32_t *len, uint8_t marker) { 285 | // per https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format 286 | unsigned const char *bytes = *start; 287 | 288 | // kinda skanky, will break if unlucky and the headers inxlucde 0xffda 289 | // might fall off array if jpeg is invalid 290 | // FIXME - return false instead 291 | while (bytes - *start < *len) { 292 | uint8_t framing = *bytes++; // better be 0xff 293 | if (framing != 0xff) { 294 | printf("malformed jpeg, framing=%x\n", framing); 295 | return false; 296 | } 297 | uint8_t typecode = *bytes++; 298 | if (typecode == marker) { 299 | unsigned skipped = bytes - *start; 300 | // printf("found marker 0x%x, skipped %d\n", marker, skipped); 301 | 302 | *start = bytes; 303 | 304 | // shrink len for the bytes we just skipped 305 | *len -= skipped; 306 | 307 | return true; 308 | } else { 309 | // not the section we were looking for, skip the entire section 310 | switch (typecode) { 311 | case 0xd8: // start of image 312 | { 313 | break; // no data to skip 314 | } 315 | case 0xe0: // app0 316 | case 0xdb: // dqt 317 | case 0xc4: // dht 318 | case 0xc0: // sof0 319 | case 0xda: // sos 320 | { 321 | // standard format section with 2 bytes for len. skip that 322 | // many bytes 323 | uint32_t len = bytes[0] * 256 + bytes[1]; 324 | // printf("skipping section 0x%x, %d bytes\n", typecode, 325 | // len); 326 | bytes += len; 327 | break; 328 | } 329 | default: 330 | printf("unexpected jpeg typecode 0x%x\n", typecode); 331 | break; 332 | } 333 | } 334 | } 335 | 336 | printf("failed to find jpeg marker 0x%x", marker); 337 | return false; 338 | } 339 | 340 | // the scan data uses byte stuffing to guarantee anything that starts with 0xff 341 | // followed by something not zero, is a new section. Look for that marker and 342 | // return the ptr pointing there 343 | void skipScanBytes(BufPtr *start) { 344 | BufPtr bytes = *start; 345 | 346 | while (true) { // FIXME, check against length 347 | while (*bytes++ != 0xff) 348 | ; 349 | if (*bytes++ != 0) { 350 | *start = bytes - 2; // back up to the 0xff marker we just found 351 | return; 352 | } 353 | } 354 | } 355 | void nextJpegBlock(BufPtr *bytes) { 356 | uint32_t len = (*bytes)[0] * 256 + (*bytes)[1]; 357 | // printf("going to next jpeg block %d bytes\n", len); 358 | *bytes += len; 359 | } 360 | 361 | // When JPEG is stored as a file it is wrapped in a container 362 | // This function fixes up the provided start ptr to point to the 363 | // actual JPEG stream data and returns the number of bytes skipped 364 | bool decodeJPEGfile(BufPtr *start, uint32_t *len, BufPtr *qtable0, 365 | BufPtr *qtable1) { 366 | // per https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format 367 | unsigned const char *bytes = *start; 368 | 369 | if (!findJPEGheader(&bytes, len, 370 | 0xd8)) // better at least look like a jpeg file 371 | return false; // FAILED! 372 | 373 | // Look for quant tables if they are present 374 | *qtable0 = NULL; 375 | *qtable1 = NULL; 376 | BufPtr quantstart = *start; 377 | uint32_t quantlen = *len; 378 | if (!findJPEGheader(&quantstart, &quantlen, 0xdb)) { 379 | printf("error can't find quant table 0\n"); 380 | } else { 381 | // printf("found quant table %x\n", quantstart[2]); 382 | 383 | *qtable0 = quantstart + 3; // 3 bytes of header skipped 384 | nextJpegBlock(&quantstart); 385 | if (!findJPEGheader(&quantstart, &quantlen, 0xdb)) { 386 | printf("error can't find quant table 1\n"); 387 | } else { 388 | // printf("found quant table %x\n", quantstart[2]); 389 | } 390 | *qtable1 = quantstart + 3; 391 | nextJpegBlock(&quantstart); 392 | } 393 | 394 | if (!findJPEGheader(start, len, 0xda)) return false; // FAILED! 395 | 396 | // Skip the header bytes of the SOS marker FIXME why doesn't this work? 397 | uint32_t soslen = (*start)[0] * 256 + (*start)[1]; 398 | *start += soslen; 399 | *len -= soslen; 400 | 401 | // start scanning the data portion of the scan to find the end marker 402 | BufPtr endmarkerptr = *start; 403 | uint32_t endlen = *len; 404 | 405 | skipScanBytes(&endmarkerptr); 406 | if (!findJPEGheader(&endmarkerptr, &endlen, 0xd9)) return false; // FAILED! 407 | 408 | // endlen must now be the # of bytes between the start of our scan and 409 | // the end marker, tell the caller to ignore bytes afterwards 410 | *len = endmarkerptr - *start; 411 | 412 | return true; 413 | } 414 | -------------------------------------------------------------------------------- /examples/rtsp_stream/CRtspSession.cpp: -------------------------------------------------------------------------------- 1 | #include "CRtspSession.h" 2 | #include 3 | #include 4 | 5 | CRtspSession::CRtspSession(WiFiClient& aClient, CStreamer* aStreamer) 6 | : LinkedListElement(aStreamer->getClientsListHead()), 7 | m_Client(aClient), 8 | m_Streamer(aStreamer) { 9 | printf("Creating RTSP session\n"); 10 | Init(); 11 | 12 | m_RtspClient = &m_Client; 13 | m_RtspSessionID = getRandom(); // create a session ID 14 | m_RtspSessionID |= 0x80000000; 15 | m_StreamID = -1; 16 | m_ClientRTPPort = 0; 17 | m_ClientRTCPPort = 0; 18 | m_TcpTransport = false; 19 | m_streaming = false; 20 | m_stopped = false; 21 | 22 | m_RtpClientPort = 0; 23 | m_RtcpClientPort = 0; 24 | }; 25 | 26 | CRtspSession::~CRtspSession() { 27 | m_Streamer->ReleaseUdpTransport(); 28 | closesocket(m_RtspClient); 29 | }; 30 | 31 | void CRtspSession::Init() { 32 | m_RtspCmdType = RTSP_UNKNOWN; 33 | memset(m_URLPreSuffix, 0x00, sizeof(m_URLPreSuffix)); 34 | memset(m_URLSuffix, 0x00, sizeof(m_URLSuffix)); 35 | memset(m_CSeq, 0x00, sizeof(m_CSeq)); 36 | memset(m_URLHostPort, 0x00, sizeof(m_URLHostPort)); 37 | m_ContentLength = 0; 38 | }; 39 | 40 | bool CRtspSession::ParseRtspRequest(char const* aRequest, 41 | unsigned aRequestSize) { 42 | char CmdName[RTSP_PARAM_STRING_MAX]; 43 | static char CurRequest[RTSP_BUFFER_SIZE]; // Note: we assume single 44 | // threaded, this large buf we 45 | // keep off of the tiny stack 46 | unsigned CurRequestSize; 47 | 48 | Init(); 49 | CurRequestSize = aRequestSize; 50 | memcpy(CurRequest, aRequest, aRequestSize); 51 | 52 | // check whether the request contains information about the RTP/RTCP UDP 53 | // client ports (SETUP command) 54 | char* ClientPortPtr; 55 | char* TmpPtr; 56 | static char CP[1024]; 57 | char* pCP; 58 | 59 | ClientPortPtr = strstr(CurRequest, "client_port"); 60 | if (ClientPortPtr != nullptr) { 61 | TmpPtr = strstr(ClientPortPtr, "\r\n"); 62 | if (TmpPtr != nullptr) { 63 | TmpPtr[0] = 0x00; 64 | strcpy(CP, ClientPortPtr); 65 | pCP = strstr(CP, "="); 66 | if (pCP != nullptr) { 67 | pCP++; 68 | strcpy(CP, pCP); 69 | pCP = strstr(CP, "-"); 70 | if (pCP != nullptr) { 71 | pCP[0] = 0x00; 72 | m_ClientRTPPort = atoi(CP); 73 | m_ClientRTCPPort = m_ClientRTPPort + 1; 74 | }; 75 | }; 76 | }; 77 | }; 78 | 79 | // Read everything up to the first space as the command name 80 | bool parseSucceeded = false; 81 | unsigned i; 82 | for (i = 0; i < sizeof(CmdName) - 1 && i < CurRequestSize; ++i) { 83 | char c = CurRequest[i]; 84 | if (c == ' ' || c == '\t') { 85 | parseSucceeded = true; 86 | break; 87 | } 88 | CmdName[i] = c; 89 | } 90 | CmdName[i] = '\0'; 91 | if (!parseSucceeded) { 92 | printf("failed to parse RTSP\n"); 93 | return false; 94 | } 95 | 96 | printf("RTSP received %s\n", CmdName); 97 | 98 | // find out the command type 99 | if (strstr(CmdName, "OPTIONS") != nullptr) 100 | m_RtspCmdType = RTSP_OPTIONS; 101 | else if (strstr(CmdName, "DESCRIBE") != nullptr) 102 | m_RtspCmdType = RTSP_DESCRIBE; 103 | else if (strstr(CmdName, "SETUP") != nullptr) 104 | m_RtspCmdType = RTSP_SETUP; 105 | else if (strstr(CmdName, "PLAY") != nullptr) 106 | m_RtspCmdType = RTSP_PLAY; 107 | else if (strstr(CmdName, "TEARDOWN") != nullptr) 108 | m_RtspCmdType = RTSP_TEARDOWN; 109 | 110 | // check whether the request contains transport information (UDP or TCP) 111 | if (m_RtspCmdType == RTSP_SETUP) { 112 | TmpPtr = strstr(CurRequest, "RTP/AVP/TCP"); 113 | if (TmpPtr != nullptr) 114 | m_TcpTransport = true; 115 | else 116 | m_TcpTransport = false; 117 | }; 118 | 119 | // Skip over the prefix of any "rtsp://" or "rtsp:/" URL that follows: 120 | unsigned j = i + 1; 121 | while (j < CurRequestSize && 122 | (CurRequest[j] == ' ' || CurRequest[j] == '\t')) 123 | ++j; // skip over any additional white space 124 | for (; (int)j < (int)(CurRequestSize - 8); ++j) { 125 | if ((CurRequest[j] == 'r' || CurRequest[j] == 'R') && 126 | (CurRequest[j + 1] == 't' || CurRequest[j + 1] == 'T') && 127 | (CurRequest[j + 2] == 's' || CurRequest[j + 2] == 'S') && 128 | (CurRequest[j + 3] == 'p' || CurRequest[j + 3] == 'P') && 129 | CurRequest[j + 4] == ':' && CurRequest[j + 5] == '/') { 130 | j += 6; 131 | if (CurRequest[j] == '/') { // This is a "rtsp://" URL; skip over 132 | // the host:port part that follows: 133 | ++j; 134 | unsigned uidx = 0; 135 | while ( 136 | j < CurRequestSize && CurRequest[j] != '/' && 137 | CurRequest[j] != ' ' && 138 | uidx < 139 | sizeof(m_URLHostPort) - 140 | 1) { // extract the host:port part of the URL here 141 | m_URLHostPort[uidx] = CurRequest[j]; 142 | uidx++; 143 | ++j; 144 | }; 145 | } else 146 | --j; 147 | i = j; 148 | break; 149 | } 150 | } 151 | 152 | // Look for the URL suffix (before the following "RTSP/"): 153 | parseSucceeded = false; 154 | for (unsigned k = i + 1; (int)k < (int)(CurRequestSize - 5); ++k) { 155 | if (CurRequest[k] == 'R' && CurRequest[k + 1] == 'T' && 156 | CurRequest[k + 2] == 'S' && CurRequest[k + 3] == 'P' && 157 | CurRequest[k + 4] == '/') { 158 | while (--k >= i && CurRequest[k] == ' ') { 159 | } 160 | unsigned k1 = k; 161 | while (k1 > i && CurRequest[k1] != '/') --k1; 162 | if (k - k1 + 1 > sizeof(m_URLSuffix)) return false; 163 | unsigned n = 0, k2 = k1 + 1; 164 | 165 | while (k2 <= k) m_URLSuffix[n++] = CurRequest[k2++]; 166 | m_URLSuffix[n] = '\0'; 167 | 168 | if (k1 - i > sizeof(m_URLPreSuffix)) return false; 169 | n = 0; 170 | k2 = i + 1; 171 | while (k2 <= k1 - 1) m_URLPreSuffix[n++] = CurRequest[k2++]; 172 | m_URLPreSuffix[n] = '\0'; 173 | i = k + 7; 174 | parseSucceeded = true; 175 | break; 176 | } 177 | } 178 | if (!parseSucceeded) return false; 179 | 180 | // Look for "CSeq:", skip whitespace, then read everything up to the next \r 181 | // or \n as 'CSeq': 182 | parseSucceeded = false; 183 | for (j = i; (int)j < (int)(CurRequestSize - 5); ++j) { 184 | if (CurRequest[j] == 'C' && CurRequest[j + 1] == 'S' && 185 | CurRequest[j + 2] == 'e' && CurRequest[j + 3] == 'q' && 186 | CurRequest[j + 4] == ':') { 187 | j += 5; 188 | while (j < CurRequestSize && 189 | (CurRequest[j] == ' ' || CurRequest[j] == '\t')) 190 | ++j; 191 | unsigned n; 192 | for (n = 0; n < sizeof(m_CSeq) - 1 && j < CurRequestSize; 193 | ++n, ++j) { 194 | char c = CurRequest[j]; 195 | if (c == '\r' || c == '\n') { 196 | parseSucceeded = true; 197 | break; 198 | } 199 | m_CSeq[n] = c; 200 | } 201 | m_CSeq[n] = '\0'; 202 | break; 203 | } 204 | } 205 | if (!parseSucceeded) return false; 206 | 207 | // Also: Look for "Content-Length:" (optional) 208 | for (j = i; (int)j < (int)(CurRequestSize - 15); ++j) { 209 | if (CurRequest[j] == 'C' && CurRequest[j + 1] == 'o' && 210 | CurRequest[j + 2] == 'n' && CurRequest[j + 3] == 't' && 211 | CurRequest[j + 4] == 'e' && CurRequest[j + 5] == 'n' && 212 | CurRequest[j + 6] == 't' && CurRequest[j + 7] == '-' && 213 | (CurRequest[j + 8] == 'L' || CurRequest[j + 8] == 'l') && 214 | CurRequest[j + 9] == 'e' && CurRequest[j + 10] == 'n' && 215 | CurRequest[j + 11] == 'g' && CurRequest[j + 12] == 't' && 216 | CurRequest[j + 13] == 'h' && CurRequest[j + 14] == ':') { 217 | j += 15; 218 | while (j < CurRequestSize && 219 | (CurRequest[j] == ' ' || CurRequest[j] == '\t')) 220 | ++j; 221 | unsigned num; 222 | if (sscanf(&CurRequest[j], "%u", &num) == 1) m_ContentLength = num; 223 | } 224 | } 225 | return true; 226 | }; 227 | 228 | RTSP_CMD_TYPES CRtspSession::Handle_RtspRequest(char const* aRequest, 229 | unsigned aRequestSize) { 230 | if (ParseRtspRequest(aRequest, aRequestSize)) { 231 | switch (m_RtspCmdType) { 232 | case RTSP_OPTIONS: { 233 | Handle_RtspOPTION(); 234 | break; 235 | }; 236 | case RTSP_DESCRIBE: { 237 | Handle_RtspDESCRIBE(); 238 | break; 239 | }; 240 | case RTSP_SETUP: { 241 | Handle_RtspSETUP(); 242 | break; 243 | }; 244 | case RTSP_PLAY: { 245 | Handle_RtspPLAY(); 246 | break; 247 | }; 248 | default: { 249 | }; 250 | }; 251 | }; 252 | return m_RtspCmdType; 253 | }; 254 | 255 | void CRtspSession::Handle_RtspOPTION() { 256 | static char Response[1024]; // Note: we assume single threaded, this large 257 | // buf we keep off of the tiny stack 258 | 259 | snprintf(Response, sizeof(Response), 260 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 261 | "Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n\r\n", 262 | m_CSeq); 263 | 264 | socketsend(m_RtspClient, Response, strlen(Response)); 265 | } 266 | 267 | void CRtspSession::Handle_RtspDESCRIBE() { 268 | static char Response[4096]; // Note: we assume single threaded, this large 269 | // buf we keep off of the tiny stack 270 | static char SDPBuf[1024]; 271 | static char URLBuf[1024]; 272 | 273 | // check whether we know a stream with the URL which is requested 274 | m_StreamID = -1; // invalid URL 275 | if ((strcmp(m_URLPreSuffix, "mjpeg") == 0) && 276 | (strcmp(m_URLSuffix, "1") == 0)) 277 | m_StreamID = 0; 278 | else if ((strcmp(m_URLPreSuffix, "mjpeg") == 0) && 279 | (strcmp(m_URLSuffix, "2") == 0)) 280 | m_StreamID = 1; 281 | if (m_StreamID == -1) { // Stream not available 282 | snprintf(Response, sizeof(Response), 283 | "RTSP/1.0 404 Stream Not Found\r\nCSeq: %s\r\n%s\r\n", m_CSeq, 284 | DateHeader()); 285 | 286 | socketsend(m_RtspClient, Response, strlen(Response)); 287 | return; 288 | }; 289 | 290 | // simulate DESCRIBE server response 291 | static char OBuf[256]; 292 | char* ColonPtr; 293 | strcpy(OBuf, m_URLHostPort); 294 | ColonPtr = strstr(OBuf, ":"); 295 | if (ColonPtr != nullptr) ColonPtr[0] = 0x00; 296 | 297 | snprintf( 298 | SDPBuf, sizeof(SDPBuf), 299 | "v=0\r\n" 300 | "o=- %d 1 IN IP4 %s\r\n" 301 | "s=\r\n" 302 | "t=0 0\r\n" // start / stop - 0 -> unbounded and permanent session 303 | "m=video 0 RTP/AVP 26\r\n" // currently we just handle UDP sessions 304 | // "a=x-dimensions: 640,480\r\n" 305 | "c=IN IP4 0.0.0.0\r\n", 306 | rand(), OBuf); 307 | char StreamName[64]; 308 | switch (m_StreamID) { 309 | case 0: 310 | strcpy(StreamName, "mjpeg/1"); 311 | break; 312 | case 1: 313 | strcpy(StreamName, "mjpeg/2"); 314 | break; 315 | }; 316 | snprintf(URLBuf, sizeof(URLBuf), "rtsp://%s/%s", m_URLHostPort, StreamName); 317 | snprintf(Response, sizeof(Response), 318 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 319 | "%s\r\n" 320 | "Content-Base: %s/\r\n" 321 | "Content-Type: application/sdp\r\n" 322 | "Content-Length: %d\r\n\r\n" 323 | "%s", 324 | m_CSeq, DateHeader(), URLBuf, (int)strlen(SDPBuf), SDPBuf); 325 | 326 | socketsend(m_RtspClient, Response, strlen(Response)); 327 | } 328 | 329 | void CRtspSession::InitTransport(u_short aRtpPort, u_short aRtcpPort) { 330 | m_RtpClientPort = aRtpPort; 331 | m_RtcpClientPort = aRtcpPort; 332 | 333 | if (!m_TcpTransport) { // allocate port pairs for RTP/RTCP ports in UDP 334 | // transport mode 335 | m_Streamer->InitUdpTransport(); 336 | }; 337 | }; 338 | 339 | void CRtspSession::Handle_RtspSETUP() { 340 | static char Response[1024]; 341 | static char Transport[255]; 342 | 343 | // init RTSP Session transport type (UDP or TCP) and ports for UDP transport 344 | InitTransport(m_ClientRTPPort, m_ClientRTCPPort); 345 | 346 | // simulate SETUP server response 347 | if (m_TcpTransport) 348 | snprintf(Transport, sizeof(Transport), 349 | "RTP/AVP/TCP;unicast;interleaved=0-1"); 350 | else 351 | snprintf(Transport, sizeof(Transport), 352 | "RTP/" 353 | "AVP;unicast;destination=127.0.0.1;source=127.0.0.1;client_" 354 | "port=%i-%i;server_port=%i-%i", 355 | m_ClientRTPPort, m_ClientRTCPPort, 356 | m_Streamer->GetRtpServerPort(), 357 | m_Streamer->GetRtcpServerPort()); 358 | snprintf(Response, sizeof(Response), 359 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 360 | "%s\r\n" 361 | "Transport: %s\r\n" 362 | "Session: %i\r\n\r\n", 363 | m_CSeq, DateHeader(), Transport, m_RtspSessionID); 364 | 365 | socketsend(m_RtspClient, Response, strlen(Response)); 366 | } 367 | 368 | void CRtspSession::Handle_RtspPLAY() { 369 | static char Response[1024]; 370 | 371 | // simulate SETUP server response 372 | snprintf(Response, sizeof(Response), 373 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 374 | "%s\r\n" 375 | "Range: npt=0.000-\r\n" 376 | "Session: %i\r\n" 377 | "RTP-Info: url=rtsp://127.0.0.1:8554/mjpeg/1/track1\r\n\r\n", 378 | m_CSeq, DateHeader(), m_RtspSessionID); 379 | 380 | socketsend(m_RtspClient, Response, strlen(Response)); 381 | } 382 | 383 | char const* CRtspSession::DateHeader() { 384 | static char buf[200]; 385 | time_t tt = time(NULL); 386 | strftime(buf, sizeof buf, "Date: %a, %b %d %Y %H:%M:%S GMT", gmtime(&tt)); 387 | return buf; 388 | } 389 | 390 | int CRtspSession::GetStreamID() { 391 | return m_StreamID; 392 | }; 393 | 394 | /** 395 | Read from our socket, parsing commands as possible. 396 | */ 397 | bool CRtspSession::handleRequests(uint32_t readTimeoutMs) { 398 | if (m_stopped) return false; // Already closed down 399 | 400 | static char 401 | RecvBuf[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this 402 | // large buf we keep off of the tiny stack 403 | 404 | memset(RecvBuf, 0x00, sizeof(RecvBuf)); 405 | int res = socketread(m_RtspClient, RecvBuf, sizeof(RecvBuf), readTimeoutMs); 406 | if (res > 0) { 407 | // we filter away everything which seems not to be an RTSP command: 408 | // O-ption, D-escribe, S-etup, P-lay, T-eardown 409 | if ((RecvBuf[0] == 'O') || (RecvBuf[0] == 'D') || (RecvBuf[0] == 'S') || 410 | (RecvBuf[0] == 'P') || (RecvBuf[0] == 'T')) { 411 | RTSP_CMD_TYPES C = Handle_RtspRequest(RecvBuf, res); 412 | if (C == RTSP_PLAY) 413 | m_streaming = true; 414 | else if (C == RTSP_TEARDOWN) 415 | m_stopped = true; 416 | } 417 | return true; 418 | } else if (res == 0) { 419 | printf("client closed socket, exiting\n"); 420 | m_stopped = true; 421 | return true; 422 | } else { 423 | // Timeout on read 424 | 425 | return false; 426 | } 427 | } 428 | --------------------------------------------------------------------------------