feat: add camera avi video recorder example
This commit is contained in:
parent
5e5c4aa34c
commit
4fa6a72bd4
20 changed files with 2332 additions and 0 deletions
15
examples/camera/video_recorder/CMakeLists.txt
Normal file
15
examples/camera/video_recorder/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
#If IOT_SOLUTION_PATH is not defined, use relative path as default value
|
||||
if(NOT DEFINED ENV{IOT_SOLUTION_PATH})
|
||||
get_filename_component(IOT_SOLUTION_PATH "${CMAKE_SOURCE_DIR}/../../.." ABSOLUTE)
|
||||
set(ENV{IOT_SOLUTION_PATH} ${IOT_SOLUTION_PATH})
|
||||
message(WARNING "Can't detect IOT_SOLUTION_PATH in your environment, we infer it is $ENV{IOT_SOLUTION_PATH}")
|
||||
endif()
|
||||
|
||||
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IOT_SOLUTION_PATH}/examples/camera/camera_components/camera_example_common" "$ENV{IOT_SOLUTION_PATH}/examples/camera/camera_components/esp32-camera")
|
||||
include($ENV{IOT_SOLUTION_PATH}/component.cmake)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(video_recorder)
|
||||
87
examples/camera/video_recorder/README.md
Normal file
87
examples/camera/video_recorder/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
# Video Recorder Example
|
||||
|
||||
This example will show how to recorder video to sdcard with avi format.
|
||||
See the [README.md](../README.md) file in the upper level [camera](../) directory for more information about examples.
|
||||
|
||||
## How to Use Example
|
||||
|
||||
### Hardware Required
|
||||
|
||||
* A development board with a camera module and an SD card slot (e.g., ESP32-CAM, ESP32-S3-EYE, etc.)
|
||||
* A USB cable for power supply and programming
|
||||
* A SDCard
|
||||
|
||||
You need to plug the SDCard into the socket on the board.
|
||||
|
||||
- Note 1:
|
||||
By default, The [SDMMC Host driver](https://docs.espressif.com/projects/esp-idf/zh_CN/v4.4.1/esp32/api-reference/peripherals/sdmmc_host.html) is used to initialize the SD card. SD cards can be used either over SPI interface (on all ESP chips) or over SDMMC interface (on ESP32 and ESP32-S3). To use SDMMC interface, enable "Use SDMMC host" (`CONFIG_EXAMPLE_USE_SDMMC_HOST`) option. To use SPI interface, disable this option.
|
||||
|
||||
The example will be able to mount only cards formatted using FAT32 filesystem. If the card is formatted as exFAT or some other filesystem, you have an option to format it in the example code — "Format the card if mount failed" (`CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED`).
|
||||
|
||||
For more information on pin configuration for SDMMC and SDSPI, check related examples: [sd card examples](https://github.com/espressif/esp-idf/tree/release/v4.4/examples/storage/sd_card)
|
||||
|
||||
- Note 2:
|
||||
|
||||
It is recommended to use 4 line SD mode to speed up the storage rate. Besides, Please format the card with an appropriate allocation unit size. Using larger allocation unit size will result in higher read/write performance and higher overhead when storing small files.
|
||||
|
||||
- Note 3:
|
||||
Currently, only JPEG image encoding formats are supported, so using a camera that supports JPEG encoding will help a lot to achieve smooth video.
|
||||
|
||||
### Configure the project
|
||||
|
||||
step1: Chose your taget chip.
|
||||
|
||||
````
|
||||
idf.py menuconfig -> Camera Pin Configuration
|
||||
````
|
||||
step2: Configure the camera.
|
||||
```
|
||||
idf.py menuconfig ->component config -> Camera Configuration
|
||||
```
|
||||
step3: Enable the HTTP file server if you want to download the video from the web page.
|
||||
|
||||
```
|
||||
idf.py menuconfig ->Example Configuration
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Build the project and flash it to the board, then run monitor tool to view serial output:
|
||||
|
||||
```
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(To exit the serial monitor, type ``Ctrl-]``.)
|
||||
|
||||
See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
|
||||
|
||||
## Working with the example
|
||||
1. Here is an example console output. In this case a 16GB SDHC card was connected, and `EXAMPLE_FILE_SERVER_ENABLED` menuconfig option enabled.
|
||||
```
|
||||
I (855) ov2640: Set PLL: clk_2x: 0, clk_div: 0, pclk_auto: 0, pclk_div: 8
|
||||
I (935) video_recorder: satrt to test fps
|
||||
I (1693) video_recorder: fps=25.010317, image_average_size=22142
|
||||
I (1693) file manager: Initializing SD card
|
||||
I (1693) file manager: Using SDMMC peripheral
|
||||
I (1693) gpio: GPIO[39]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1693) gpio: GPIO[38]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
I (1695) gpio: GPIO[40]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0
|
||||
Name: SC16G
|
||||
Type: SDHC/SDXC
|
||||
Speed: 20 MHz
|
||||
Size: 15193MB
|
||||
I (1755) avi recorder: Starting an avi [/sdcard/recorde.avi]
|
||||
I (21775) avi recorder: video info: width=640 | height=480 | fps=25
|
||||
I (21785) avi recorder: frame number=502, size=9482KB
|
||||
I (21849) avi recorder: avi recording completed
|
||||
```
|
||||
2. Note down the IP assigned to your ESP module. The IP address is logged by the example as follows:
|
||||
```
|
||||
I (5424) example_connect: - IPv4 address: 192.168.1.100
|
||||
I (5424) example_connect: - IPv6 address: fe80:0000:0000:0000:86f7:03ff:fec0:1620, type: ESP_IP6_ADDR_IS
|
||||
The following steps assume that IP address 192.168.1.100 was assigned.
|
||||
```
|
||||
3. Test the example interactively in a web browser. The default port is 80.
|
||||
|
||||
Open path `http://192.168.1.100/` or `http://192.168.1.100/index.html` to see an HTML page with list of files on the server.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
idf_component_register(SRCS avi_recorder.c
|
||||
INCLUDE_DIRS .)
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#ifndef _AVI_DEFINE_H_
|
||||
#define _AVI_DEFINE_H_
|
||||
|
||||
#include "stdint.h"
|
||||
|
||||
/**
|
||||
* reference links: https://www.cnblogs.com/songhe364826110/p/7619949.html
|
||||
* AVIFileFormat: https://web.archive.org/web/20170411001412/http://www.alexander-noe.com/video/documentation/avi.pdf
|
||||
* OpenDML AVI File Format Extensions: https://web.archive.org/web/20070112225112/http://www.the-labs.com/Video/odmlff2-avidef.pdf
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
uint32_t FourCC;
|
||||
uint32_t size; // Block size, equal to the size of subsequent data
|
||||
/* data uint8_t _data[size]; */
|
||||
}AVI_CHUNK_HEAD;
|
||||
|
||||
typedef struct {
|
||||
uint32_t List; // Fixed to "list" and "riff" if it is riff list
|
||||
uint32_t size; // Block size, equal to the size of subsequent data
|
||||
uint32_t FourCC;
|
||||
/* data uint8_t _data[size-4]; */
|
||||
}AVI_LIST_HEAD;
|
||||
|
||||
typedef struct {
|
||||
uint32_t FourCC; // Block ID, fixed to 'avih'
|
||||
uint32_t size; // Block size = size of struct avi_avih_chunk - the size of ID and size.
|
||||
uint32_t us_per_frame; // Video frame interval (in microseconds)
|
||||
uint32_t max_bytes_per_sec; // Maximum data rate of AVI file
|
||||
uint32_t padding; // Set to 0
|
||||
uint32_t flags; // Global attributes of AVI file, such as whether it contains index blocks, whether audio and video data are cross stored, etc
|
||||
uint32_t total_frames; // Total frames
|
||||
uint32_t init_frames; // Specify the initial number of frames for interactive format (non interactive format should be specified as 0)
|
||||
uint32_t streams; // The number of streams contained in the file. When there is only video stream, it is 1
|
||||
uint32_t suggest_buff_size; // Specifies the buffer size recommended for reading this file. It is usually the sum of the data required to store a frame image and synchronize sound. If it is not specified, it is set to 0
|
||||
uint32_t width; // Width of video main window (unit: pixel)
|
||||
uint32_t height; // Height of video main window (unit: pixel)
|
||||
uint32_t reserved[4]; // reserved space for dwScale,dwRate,dwStart,dwLength.
|
||||
}AVI_AVIH_CHUNK;
|
||||
|
||||
typedef struct {
|
||||
int16_t left;
|
||||
int16_t top;
|
||||
int16_t right;
|
||||
int16_t bottom;
|
||||
}AVI_RECT_FRAME;
|
||||
|
||||
typedef struct {
|
||||
uint32_t FourCC; // Block ID, set to Strh
|
||||
uint32_t size; // Block size = size of struct struct avi_strh_chunk - the size of ID and size
|
||||
uint32_t fourcc_type; // The type of stream. vids represents video stream and auds represents audio stream
|
||||
uint32_t fourcc_codec; // Specify the decoder needed to process this stream, such as JPEG
|
||||
uint32_t flags; // Flag, such as whether to allow this stream output, whether the palette changes, etc., is generally set to 0
|
||||
uint16_t priority; // The priority of the video stream can be set to 0
|
||||
uint16_t language; // Audio language code, video stream can be set to 0
|
||||
uint32_t init_frames; // Specify the initial number of frames for interactive format (non interactive format should be specified as 0)
|
||||
uint32_t scale; //
|
||||
uint32_t rate; // For video streams,rate / scale = fps (frame rate)
|
||||
uint32_t start; // For video streams, set to 0
|
||||
uint32_t length; // For video streams, length is the total number of frames
|
||||
uint32_t suggest_buff_size; // The recommended buffer size for reading this stream data
|
||||
uint32_t quality; // Quality index of stream data
|
||||
uint32_t sample_size; // Audio sampling size, video stream can be set to 0
|
||||
AVI_RECT_FRAME rcFrame; // The display position of this stream in the main video window can be set to {0,0, width, height}
|
||||
}AVI_STRH_CHUNK;
|
||||
|
||||
/*For video streams, the STRF block structure is as follows*/
|
||||
typedef struct {
|
||||
uint32_t FourCC; // Block ID, set to 'strf'
|
||||
uint32_t size; // Block size = size of struct struct avi_strh_chunk - the size of ID and size
|
||||
uint32_t size1; // Size1 has the same meaning and value as 'size' above
|
||||
uint32_t width; // Width of video main window (unit: pixel)
|
||||
uint32_t height; // Height of video main window (unit: pixel)
|
||||
uint16_t planes; // set to 1
|
||||
uint16_t bitcount; // The number of bits occupied by each pixel can only be one of 1, 4, 8, 16, 24 and 32
|
||||
uint32_t fourcc_compression; // Video stream coding format, such as "JPEG", "mjpg", etc
|
||||
uint32_t image_size; // Video image size = width * height * bitcount / 8
|
||||
uint32_t x_pixels_per_meter; // The horizontal resolution of the display device can be set to 0
|
||||
uint32_t y_pixels_per_meter; // The vertical resolution of the display device can be set to 0
|
||||
uint32_t num_colors; // Set to 0
|
||||
uint32_t imp_colors; // Set to 0
|
||||
}AVI_VIDS_STRF_CHUNK;
|
||||
|
||||
/*For audio streams, the strf block structure is as follows*/
|
||||
typedef struct __attribute__((packed))
|
||||
{
|
||||
uint32_t FourCC; // blcok ID, for audio it is 'strf'
|
||||
uint32_t size; // Block size = size of struct struct avi_strh_chunk - the size of ID and size
|
||||
uint16_t format_tag;
|
||||
uint16_t channels;
|
||||
uint32_t samples_per_sec;
|
||||
uint32_t avg_bytes_per_sec;
|
||||
uint16_t block_align;
|
||||
uint32_t bits_per_sample;
|
||||
}AVI_AUDS_STRF_CHUNK;
|
||||
|
||||
typedef struct {
|
||||
AVI_LIST_HEAD strl;
|
||||
AVI_STRH_CHUNK strh;
|
||||
AVI_VIDS_STRF_CHUNK strf;
|
||||
}AVI_STRL_LIST;
|
||||
|
||||
typedef struct {
|
||||
AVI_LIST_HEAD hdrl;
|
||||
AVI_AVIH_CHUNK avih;
|
||||
AVI_STRL_LIST strl;
|
||||
}AVI_HDRL_LIST;
|
||||
|
||||
typedef struct {
|
||||
uint32_t FourCC; // Block ID,can be set to "idx1"
|
||||
uint32_t flags;
|
||||
uint32_t chunkoffset;
|
||||
uint32_t chunklength;
|
||||
}AVI_IDX1;
|
||||
|
||||
/**
|
||||
"db":Uncompressed video frame (RGB data stream);
|
||||
"dc":Compressed video frames;
|
||||
"wb":Audio uncompressed data (wave stream);
|
||||
"wc":Audio compressed data (compressed wave data stream);
|
||||
"pc":Use a new palette instead. (the new palette is defined by a data structure avipalchange. If the palette of a stream may change halfway, an avisf_video_palchanges tag should be included in the description of the stream format, that is, the dwflags of avistreamreader structure.)
|
||||
*/
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "string.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "avi_def.h"
|
||||
|
||||
static const char *TAG = "avi recorder";
|
||||
|
||||
#define MEM_ALIGNMENT 4
|
||||
#define MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1) & ~(MEM_ALIGNMENT-1))
|
||||
#define CUSTOM_CHUNK_SIZE (16*1024)
|
||||
|
||||
#define MAKE_FOURCC(a, b, c, d) ((uint32_t)(d)<<24 | (uint32_t)(c)<<16 | (uint32_t)(b)<<8 | (uint32_t)(a))
|
||||
|
||||
typedef struct {
|
||||
const char *fname; // file name
|
||||
int (*get_frame)(void **buf, size_t *len);
|
||||
int (*return_frame)(void *buf);
|
||||
uint16_t image_width;
|
||||
uint16_t image_high;
|
||||
uint32_t rec_time;
|
||||
EventGroupHandle_t event_hdl;
|
||||
} recorder_param_t;
|
||||
|
||||
typedef struct {
|
||||
char filename[64]; // filename for temporary
|
||||
int avifile; // avi file fd
|
||||
int idxfile; // inx file fd, .inx file used to storage the size of each image
|
||||
uint32_t nframes; // the number of frame
|
||||
uint32_t totalsize; // all frame image size
|
||||
|
||||
// buffer for preformence
|
||||
uint8_t *buffer;
|
||||
uint32_t buf_len;
|
||||
uint32_t write_len; // the buffer has been used, or length of temporarily stored data to be processed.
|
||||
} jpeg2avi_data_t;
|
||||
|
||||
typedef enum {
|
||||
REC_STATE_IDLE,
|
||||
REC_STATE_BUSY,
|
||||
} record_state_t;
|
||||
|
||||
// #define USE_MULTI_FRAMES // Enable this if you want get mutil frames first, and then storage them to SD Card
|
||||
#ifdef USE_MULTI_FRAMES
|
||||
typedef struct {
|
||||
uint8_t **buffer;
|
||||
uint32_t buf_len;
|
||||
} jpeg2avi_frame_array_t;
|
||||
#define FRAME_ARRAY_SIZE (CONFIG_EXAMPLE_FB_COUNT/2 + 1) // get a set of images(not one), and then write to SD card to improve speed.
|
||||
#endif
|
||||
|
||||
static uint8_t g_force_end = 0;
|
||||
static record_state_t g_state = REC_STATE_IDLE;
|
||||
|
||||
static int jpeg2avi_start(jpeg2avi_data_t *j2a, const char *filename)
|
||||
{
|
||||
ESP_LOGI(TAG, "Starting an avi [%s]", filename);
|
||||
if (strlen(filename) > sizeof(j2a->filename) - 5) { // 5 is for '.avi' suffix.
|
||||
ESP_LOGE(TAG, "The given file name is too long");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
j2a->buf_len = CUSTOM_CHUNK_SIZE + sizeof(AVI_CHUNK_HEAD);
|
||||
j2a->buffer = heap_caps_malloc(j2a->buf_len, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); // for now, we request for inter dram space to improve speed
|
||||
if (NULL == j2a->buffer) {
|
||||
ESP_LOGE(TAG, "recorder mem failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
j2a->write_len = 0;
|
||||
|
||||
memset(j2a->filename, 0, sizeof(j2a->filename));
|
||||
strcpy(j2a->filename, filename);
|
||||
|
||||
j2a->avifile = open(j2a->filename, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
|
||||
if (j2a->avifile == -1) {
|
||||
ESP_LOGE(TAG, "Could not open %s (%s)", j2a->filename, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
strcat(j2a->filename, ".idx");
|
||||
j2a->idxfile = open(j2a->filename, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU);
|
||||
if (j2a->idxfile == -1) {
|
||||
ESP_LOGE(TAG, "Could not open %s (%s)", j2a->filename, strerror(errno));
|
||||
close(j2a->avifile);
|
||||
unlink(filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint32_t offset1 = sizeof(AVI_LIST_HEAD); // the size of riff head
|
||||
uint32_t offset2 = sizeof(AVI_HDRL_LIST); // the size of hdrl list
|
||||
uint32_t offset3 = sizeof(AVI_LIST_HEAD); // the size of movi list head
|
||||
|
||||
// After the offset of the AVI file is set to the movi list head, the JPEG data is written backwards from this position.
|
||||
int ret = lseek(j2a->avifile, offset1 + offset2 + offset3, SEEK_SET);
|
||||
if (-1 == ret) {
|
||||
ESP_LOGE(TAG, "seek avi file failed");
|
||||
close(j2a->avifile);
|
||||
close(j2a->idxfile);
|
||||
unlink(filename);
|
||||
unlink(j2a->filename);
|
||||
return -1;
|
||||
}
|
||||
|
||||
j2a->nframes = 0;
|
||||
j2a->totalsize = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int jpeg2avi_add_frame(jpeg2avi_data_t *j2a, uint8_t *data, uint32_t len)
|
||||
{
|
||||
int ret = 0;
|
||||
AVI_CHUNK_HEAD frame_head;
|
||||
uint32_t align_size = MEM_ALIGN_SIZE(len); // To make JPEG image size 4-byte alignment.
|
||||
const int CHUNK_SIZE = CUSTOM_CHUNK_SIZE;
|
||||
|
||||
frame_head.FourCC = MAKE_FOURCC('0', '0', 'd', 'c'); // '00dc' means that it is a compressed video data.
|
||||
frame_head.size = align_size;
|
||||
|
||||
// the total size need to be write after get a new frame:[last remain data + chunk_head + new frame data]
|
||||
uint32_t remain = j2a->write_len + align_size + sizeof(AVI_CHUNK_HEAD);
|
||||
|
||||
uint32_t last_remain = j2a->write_len;
|
||||
while (remain) {
|
||||
// int wl = remain >= CHUNK_SIZE ? CHUNK_SIZE : remain;
|
||||
/*To improve efficiency, we always get the specified amount of data before actually writing the data to the file.
|
||||
*The j2a->buffer[] is:
|
||||
---------------------------------------------------------------------
|
||||
| CHUNK_SIZE | AVI_CHUNK_HEAD_SIZE |
|
||||
---------------------------------------------------------------------
|
||||
^ write_len
|
||||
*/
|
||||
if (remain >= CHUNK_SIZE) {
|
||||
memcpy(&j2a->buffer[j2a->write_len], &frame_head, sizeof(AVI_CHUNK_HEAD)); // Copy the header data to the back of the current data to be processed
|
||||
j2a->write_len += sizeof(AVI_CHUNK_HEAD);
|
||||
int _len = CHUNK_SIZE - last_remain - sizeof(AVI_CHUNK_HEAD);
|
||||
memcpy(&j2a->buffer[j2a->write_len], data, _len);
|
||||
j2a->write_len += _len;
|
||||
data += _len;
|
||||
write(j2a->avifile, j2a->buffer, j2a->write_len);
|
||||
|
||||
remain -= j2a->write_len;
|
||||
j2a->write_len = 0;
|
||||
if (remain >= CHUNK_SIZE) {
|
||||
// aligne the remain data to `CHUNK_SIZE` and then write to the avifile.
|
||||
int count = remain / CHUNK_SIZE;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
write(j2a->avifile, data, CHUNK_SIZE);
|
||||
data += CHUNK_SIZE;
|
||||
remain -= CHUNK_SIZE;
|
||||
}
|
||||
}
|
||||
memcpy(&j2a->buffer[j2a->write_len], data, remain);
|
||||
j2a->write_len += remain;
|
||||
remain = 0;
|
||||
} else {
|
||||
// the remain is [last remain data + chunk_head + new frame data]
|
||||
memcpy(&j2a->buffer[j2a->write_len], &frame_head, sizeof(AVI_CHUNK_HEAD)); // add the avi_chunk_header
|
||||
j2a->write_len += sizeof(AVI_CHUNK_HEAD);
|
||||
memcpy(&j2a->buffer[j2a->write_len], data, remain - sizeof(AVI_CHUNK_HEAD)); // add the new frame data
|
||||
j2a->write_len += remain - sizeof(AVI_CHUNK_HEAD);
|
||||
remain = 0;
|
||||
}
|
||||
}
|
||||
|
||||
write(j2a->idxfile, &align_size, 4);/*Save the 4-byte aligned JPEG image size to idx file*/
|
||||
j2a->nframes += 1;
|
||||
j2a->totalsize += align_size;
|
||||
return ret;
|
||||
}
|
||||
|
||||
#ifdef USE_MULTI_FRAMES
|
||||
static int jpeg2avi_add_frame_array(recorder_param_t *rec_arg, jpeg2avi_data_t *j2a, jpeg2avi_frame_array_t *frames, size_t frame_count)
|
||||
{
|
||||
for (size_t i = 0; i < frame_count; i++) {
|
||||
if (jpeg2avi_add_frame(j2a, *frames[i].buffer, frames[i].buf_len)) {
|
||||
ESP_LOGE(TAG, "add frames failed");
|
||||
return -1;
|
||||
}
|
||||
#ifdef FRAME_RETURN_ENABLE
|
||||
rec_arg->return_frame(frames[i].buffer);
|
||||
#endif
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int jpeg2avi_write_header(jpeg2avi_data_t *j2a, uint32_t width, uint32_t height, uint32_t fps)
|
||||
{
|
||||
|
||||
AVI_LIST_HEAD riff_head = {
|
||||
.List = MAKE_FOURCC('R', 'I', 'F', 'F'),
|
||||
// The size is 'AVI ' + hdrl + movi + index; hdrl = avih + strl; movi = n * (sizeof(AVI_CHUNK_HEAD) + aligned_image_data)
|
||||
.size = 4 + sizeof(AVI_HDRL_LIST) + sizeof(AVI_LIST_HEAD) + j2a->nframes * 8 + j2a->totalsize + (sizeof(AVI_IDX1) * j2a->nframes) + 8,
|
||||
.FourCC = MAKE_FOURCC('A', 'V', 'I', ' ')
|
||||
};
|
||||
|
||||
AVI_HDRL_LIST hdrl_list = {
|
||||
{
|
||||
.List = MAKE_FOURCC('L', 'I', 'S', 'T'),
|
||||
.size = sizeof(AVI_HDRL_LIST) - 8,
|
||||
.FourCC = MAKE_FOURCC('h', 'd', 'r', 'l'),
|
||||
},
|
||||
{
|
||||
.FourCC = MAKE_FOURCC('a', 'v', 'i', 'h'),
|
||||
.size = sizeof(AVI_AVIH_CHUNK) - 8,
|
||||
.us_per_frame = 1000000 / fps,
|
||||
.max_bytes_per_sec = (width * height * 2) / 10,
|
||||
.padding = 0,
|
||||
.flags = 0,
|
||||
.total_frames = j2a->nframes,
|
||||
.init_frames = 0,
|
||||
.streams = 1,
|
||||
.suggest_buff_size = (width * height * 2),
|
||||
.width = width,
|
||||
.height = height,
|
||||
.reserved = {0, 0, 0, 0},
|
||||
},
|
||||
{
|
||||
{
|
||||
.List = MAKE_FOURCC('L', 'I', 'S', 'T'),
|
||||
.size = sizeof(AVI_STRL_LIST) - 8,
|
||||
.FourCC = MAKE_FOURCC('s', 't', 'r', 'l'),
|
||||
},
|
||||
{
|
||||
.FourCC = MAKE_FOURCC('s', 't', 'r', 'h'),
|
||||
.size = sizeof(AVI_STRH_CHUNK) - 8,
|
||||
.fourcc_type = MAKE_FOURCC('v', 'i', 'd', 's'),
|
||||
.fourcc_codec = MAKE_FOURCC('M', 'J', 'P', 'G'),
|
||||
.flags = 0,
|
||||
.priority = 0,
|
||||
.language = 0,
|
||||
.init_frames = 0,
|
||||
.scale = 1,
|
||||
.rate = fps, //rate / scale = fps
|
||||
.start = 0,
|
||||
.length = j2a->nframes,
|
||||
.suggest_buff_size = (width * height * 2),
|
||||
.quality = 1,
|
||||
.sample_size = 0,
|
||||
.rcFrame = {0, 0, width, height},
|
||||
},
|
||||
{
|
||||
.FourCC = MAKE_FOURCC('s', 't', 'r', 'f'),
|
||||
.size = sizeof(AVI_VIDS_STRF_CHUNK) - 8,
|
||||
.size1 = sizeof(AVI_VIDS_STRF_CHUNK) - 8,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.planes = 1,
|
||||
.bitcount = 24,
|
||||
.fourcc_compression = MAKE_FOURCC('M', 'J', 'P', 'G'),
|
||||
.image_size = width * height * 3,
|
||||
.x_pixels_per_meter = 0,
|
||||
.y_pixels_per_meter = 0,
|
||||
.num_colors = 0,
|
||||
.imp_colors = 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AVI_LIST_HEAD movi_list_head = {
|
||||
.List = MAKE_FOURCC('L', 'I', 'S', 'T'),
|
||||
.size = 4 + j2a->nframes * 8 + j2a->totalsize,
|
||||
.FourCC = MAKE_FOURCC('m', 'o', 'v', 'i')
|
||||
};
|
||||
|
||||
//Relocate to the file header and backfill each block of data
|
||||
lseek(j2a->avifile, 0, SEEK_SET);
|
||||
write(j2a->avifile, &riff_head, sizeof(AVI_LIST_HEAD));
|
||||
write(j2a->avifile, &hdrl_list, sizeof(AVI_HDRL_LIST));
|
||||
write(j2a->avifile, &movi_list_head, sizeof(AVI_LIST_HEAD));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int jpeg2avi_write_index_chunk(jpeg2avi_data_t *j2a)
|
||||
{
|
||||
size_t i;
|
||||
uint32_t index = MAKE_FOURCC('i', 'd', 'x', '1'); // index chunk default fourcc
|
||||
uint32_t index_chunk_size = sizeof(AVI_IDX1) * j2a->nframes; // the total size of avi index data
|
||||
uint32_t offset = 4;
|
||||
uint32_t frame_size;
|
||||
AVI_IDX1 idx;
|
||||
|
||||
lseek(j2a->idxfile, 0, SEEK_SET);
|
||||
ESP_LOGI(TAG, "frame number=%d, size=%dKB", j2a->nframes, j2a->totalsize / 1024);
|
||||
write(j2a->avifile, &index, 4);
|
||||
write(j2a->avifile, &index_chunk_size, 4);
|
||||
|
||||
idx.FourCC = MAKE_FOURCC('0', '0', 'd', 'c'); //00dc = compressed video data
|
||||
for (i = 0; i < j2a->nframes; i++) {
|
||||
read(j2a->idxfile, &frame_size, 4); // Read size of each jpeg image
|
||||
idx.flags = 0x10; // 0x10 indicates that the current frame is a keyframe(golden frame).
|
||||
idx.chunkoffset = offset;
|
||||
idx.chunklength = frame_size;
|
||||
write(j2a->avifile, &idx, sizeof(AVI_IDX1));
|
||||
|
||||
offset = offset + frame_size + 8;
|
||||
}
|
||||
close(j2a->idxfile);
|
||||
unlink(j2a->filename);
|
||||
if (i != j2a->nframes) {
|
||||
ESP_LOGE(TAG, "avi index write failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void jpeg2avi_end(jpeg2avi_data_t *j2a, int width, int height, uint32_t fps)
|
||||
{
|
||||
ESP_LOGI(TAG, "video info: width=%d | height=%d | fps=%u", width, height, fps);
|
||||
if (j2a->write_len) { // If there is still data in the current buffer, write all to the file
|
||||
write(j2a->avifile, j2a->buffer, j2a->write_len);
|
||||
}
|
||||
|
||||
jpeg2avi_write_index_chunk(j2a); // write index block to the avi file
|
||||
jpeg2avi_write_header(j2a, width, height, fps); // file the avi file header data.
|
||||
close(j2a->avifile);
|
||||
free(j2a->buffer);
|
||||
|
||||
ESP_LOGI(TAG, "avi recording completed");
|
||||
}
|
||||
|
||||
static void recorder_task(void *args)
|
||||
{
|
||||
int ret;
|
||||
recorder_param_t *rec_arg = (recorder_param_t *)args;
|
||||
|
||||
jpeg2avi_data_t avi_recoder;
|
||||
ret = jpeg2avi_start(&avi_recoder, rec_arg->fname);
|
||||
if (0 != ret) {
|
||||
ESP_LOGE(TAG, "start failed");
|
||||
goto err;
|
||||
}
|
||||
|
||||
g_state = REC_STATE_BUSY;
|
||||
|
||||
uint64_t fr_start = esp_timer_get_time() / 1000;
|
||||
uint64_t end_time = rec_arg->rec_time * 1000 + fr_start;
|
||||
uint64_t printf_time = fr_start;
|
||||
while (1) {
|
||||
#ifndef USE_MULTI_FRAMES
|
||||
uint8_t **buffer;
|
||||
size_t len;
|
||||
ret = rec_arg->get_frame((void **)&buffer, &len);
|
||||
if (0 != ret) {
|
||||
ESP_LOGE(TAG, "get frame failed");
|
||||
vTaskDelay(5000 / portTICK_PERIOD_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
ret = jpeg2avi_add_frame(&avi_recoder, *buffer, len);
|
||||
|
||||
if (rec_arg->return_frame) {
|
||||
rec_arg->return_frame(buffer);
|
||||
}
|
||||
#else
|
||||
jpeg2avi_frame_array_t s_frames[FRAME_ARRAY_SIZE];
|
||||
for (size_t i = 0; i < FRAME_ARRAY_SIZE; i++) {
|
||||
ret = rec_arg->get_frame((void **)&s_frames[i].buffer, &s_frames[i].buf_len);
|
||||
if (0 != ret) {
|
||||
ESP_LOGE(TAG, "get frame failed");
|
||||
vTaskDelay(200 / portTICK_PERIOD_MS);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ret = jpeg2avi_add_frame_array(rec_arg, &avi_recoder, s_frames, (sizeof(s_frames) / sizeof(jpeg2avi_frame_array_t)));
|
||||
|
||||
for (size_t i = 0; i < FRAME_ARRAY_SIZE; i++) {
|
||||
rec_arg->return_frame(s_frames[i].buffer);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (0 != ret) {
|
||||
break;
|
||||
}
|
||||
|
||||
uint64_t t = esp_timer_get_time() / 1000;
|
||||
if (t - printf_time > 1000) {
|
||||
printf_time = t;
|
||||
ESP_LOGD(TAG, "recording %d/%d s", (uint32_t)((t - fr_start) / 1000), rec_arg->rec_time);
|
||||
}
|
||||
if (t > end_time || g_force_end) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint32_t fps = avi_recoder.nframes * 1000 / (esp_timer_get_time() / 1000 - fr_start);
|
||||
jpeg2avi_end(&avi_recoder, rec_arg->image_width, rec_arg->image_high, fps);
|
||||
err:
|
||||
g_state = REC_STATE_IDLE;
|
||||
g_force_end = 0;
|
||||
if (rec_arg->event_hdl) {
|
||||
xEventGroupSetBits(rec_arg->event_hdl, BIT2);
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
esp_err_t avi_recorder_start(const char *fname,
|
||||
int (*get_frame)(void **buf, size_t *len),
|
||||
int (*return_frame)(void *buf),
|
||||
uint16_t image_width,
|
||||
uint16_t image_high,
|
||||
uint32_t rec_time,
|
||||
bool block)
|
||||
{
|
||||
if (REC_STATE_IDLE != g_state) {
|
||||
ESP_LOGE(TAG, "recorder already running");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (NULL == get_frame) {
|
||||
ESP_LOGE(TAG, "recorder get frame function is invalid");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
g_force_end = 0;
|
||||
static recorder_param_t rec_arg = {0};
|
||||
rec_arg.get_frame = get_frame;
|
||||
rec_arg.return_frame = return_frame;
|
||||
rec_arg.image_width = image_width;
|
||||
rec_arg.image_high = image_high;
|
||||
rec_arg.fname = fname;
|
||||
rec_arg.rec_time = rec_time;
|
||||
rec_arg.event_hdl = xEventGroupCreate();
|
||||
|
||||
if(pdPASS != xTaskCreatePinnedToCore(recorder_task, "recorder", 1024 * 4, &rec_arg, configMAX_PRIORITIES - 2, NULL, 1)) {
|
||||
vEventGroupDelete(rec_arg.event_hdl);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
g_state = REC_STATE_BUSY;
|
||||
|
||||
if (block) {
|
||||
xEventGroupWaitBits(rec_arg.event_hdl, BIT2, pdTRUE, pdTRUE, portMAX_DELAY);
|
||||
}
|
||||
vEventGroupDelete(rec_arg.event_hdl);
|
||||
rec_arg.event_hdl = NULL;
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void avi_recorder_stop(void)
|
||||
{
|
||||
if (REC_STATE_BUSY != g_state) {
|
||||
ESP_LOGE(TAG, "recorder already idle");
|
||||
return;
|
||||
}
|
||||
g_force_end = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#ifndef _AVI_RECORDER_H_
|
||||
#define _AVI_RECORDER_H_
|
||||
|
||||
#include "esp_camera.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Start avi video recorder
|
||||
*
|
||||
* @param[in] fname video file name
|
||||
* @param[in] get_frame function to get frames
|
||||
* @param[in] return_frame function to free the frame
|
||||
* @param[in] image_width the width of the image width
|
||||
* @param[in] image_high the width of the image high
|
||||
* @param[in] rec_time the time the video was recorded
|
||||
* @param[in] block if true, block here to wait video recoredr done
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK Success
|
||||
* - ESP_ERR_INVALID_ARG Parameter error
|
||||
* - ESP_FAIL Driver installation error
|
||||
*/
|
||||
esp_err_t avi_recorder_start(const char *fname,
|
||||
int (*get_frame)(void **buf, size_t *len),
|
||||
int (*return_frame)(void *buf),
|
||||
uint16_t image_width,
|
||||
uint16_t image_high,
|
||||
uint32_t rec_time,
|
||||
bool block);
|
||||
|
||||
/**
|
||||
* @brief Stop avi recorder
|
||||
*
|
||||
*/
|
||||
void avi_recorder_stop(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
COMPONENT_ADD_INCLUDEDIRS := .
|
||||
COMPONENT_SRCDIRS := .
|
||||
5
examples/camera/video_recorder/main/CMakeLists.txt
Normal file
5
examples/camera/video_recorder/main/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
idf_component_register(SRC_DIRS "."
|
||||
INCLUDE_DIRS "."
|
||||
EMBED_FILES "favicon.ico" "upload_script.html"
|
||||
PRIV_REQUIRES esp32-camera nvs_flash esp_http_server camera_example_common fatfs spiffs avi_video_process)
|
||||
|
||||
81
examples/camera/video_recorder/main/Kconfig.projbuild
Normal file
81
examples/camera/video_recorder/main/Kconfig.projbuild
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
menu "Example Configuration"
|
||||
menu "Connection Configuration"
|
||||
config ESP_WIFI_SSID
|
||||
string "WiFi STA SSID"
|
||||
default ""
|
||||
help
|
||||
WiFi SSID (network name) to connect to or empty for Off.
|
||||
|
||||
config ESP_WIFI_PASSWORD
|
||||
string "WiFi STA Password"
|
||||
default ""
|
||||
help
|
||||
WiFi Password if WEP/WPA/WPA2 or empty if Open.
|
||||
|
||||
config ESP_WIFI_AP_SSID
|
||||
string "WiFi AP SSID"
|
||||
default "ESP32-Camera"
|
||||
help
|
||||
AP SSID (network name) to create or empty for Off.
|
||||
|
||||
config ESP_WIFI_AP_PASSWORD
|
||||
string "WiFi AP Password"
|
||||
default ""
|
||||
help
|
||||
AP password for WPA2 or empty for Open.
|
||||
|
||||
config MAX_STA_CONN
|
||||
int "Maximal STA connections"
|
||||
default 1
|
||||
help
|
||||
Max number of the STA connects to AP.
|
||||
|
||||
config ESP_WIFI_AP_CHANNEL
|
||||
string "WiFi AP Channel"
|
||||
default ""
|
||||
help
|
||||
AP channel for better connection performance.
|
||||
|
||||
config SERVER_IP
|
||||
string "WiFi AP IP Address"
|
||||
default "192.168.4.1"
|
||||
help
|
||||
IP address that the ESP will assign to it's AP interface. You can use this IP to connect to the camera after flashing.
|
||||
|
||||
config ESP_MAXIMUM_RETRY
|
||||
int "Maximum retry"
|
||||
default 5
|
||||
help
|
||||
Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent.
|
||||
|
||||
config EXAMPLE_HTTPD_CONN_CLOSE_HEADER
|
||||
bool "Send connection close header from request handlers"
|
||||
default y
|
||||
help
|
||||
If this config item is set, Connection: close header will be set in handlers.
|
||||
This closes HTTP connection and frees the server socket instantly.
|
||||
endmenu
|
||||
|
||||
menu "SD Configuration"
|
||||
config EXAMPLE_FORMAT_IF_MOUNT_SDCARD_FAILED
|
||||
bool "The card will be formatted if mount has failed."
|
||||
default n
|
||||
help
|
||||
If this config item is set, the card will be formatted if mount has failed.
|
||||
|
||||
config EXAMPLE_USE_SDMMC_HOST
|
||||
bool "Use SDMMC host"
|
||||
default y
|
||||
depends on IDF_TARGET_ESP32S3 || IDF_TARGET_ESP32
|
||||
help
|
||||
If this config item is set, SDMMC is used to mount the SDcard.
|
||||
Otherwise, will use SPI host to access and mount the SDcard.
|
||||
endmenu
|
||||
|
||||
config EXAMPLE_FILE_SERVER_ENABLED
|
||||
bool "Enable file server to download video recorder."
|
||||
default n
|
||||
help
|
||||
If this config item is set, After the video capture is over, a file server is automatically started.
|
||||
Video files can be downloaded through this file server.
|
||||
endmenu
|
||||
165
examples/camera/video_recorder/main/app_main.c
Normal file
165
examples/camera/video_recorder/main/app_main.c
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
/* video recorder Example
|
||||
|
||||
This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, this
|
||||
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "string.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include "camera_pin.h"
|
||||
#include "esp_camera.h"
|
||||
#include "file_manager.h"
|
||||
#include "avi_recorder.h"
|
||||
#include "app_wifi.h"
|
||||
|
||||
#include "sensor.h"
|
||||
|
||||
#define EXAMPLE_SENSOR_FRAME_SIZE FRAMESIZE_VGA
|
||||
#define EXAMPLE_FB_COUNT (8)
|
||||
|
||||
static const char *TAG = "video_recorder";
|
||||
|
||||
esp_err_t camera_init(uint32_t mclk_freq, const pixformat_t pixel_fromat, const framesize_t frame_size, size_t frame_buffer_count)
|
||||
{
|
||||
camera_config_t camera_config = {
|
||||
.pin_pwdn = CAMERA_PIN_PWDN,
|
||||
.pin_reset = CAMERA_PIN_RESET,
|
||||
.pin_xclk = CAMERA_PIN_XCLK,
|
||||
.pin_sscb_sda = CAMERA_PIN_SIOD,
|
||||
.pin_sscb_scl = CAMERA_PIN_SIOC,
|
||||
|
||||
.pin_d7 = CAMERA_PIN_D7,
|
||||
.pin_d6 = CAMERA_PIN_D6,
|
||||
.pin_d5 = CAMERA_PIN_D5,
|
||||
.pin_d4 = CAMERA_PIN_D4,
|
||||
.pin_d3 = CAMERA_PIN_D3,
|
||||
.pin_d2 = CAMERA_PIN_D2,
|
||||
.pin_d1 = CAMERA_PIN_D1,
|
||||
.pin_d0 = CAMERA_PIN_D0,
|
||||
.pin_vsync = CAMERA_PIN_VSYNC,
|
||||
.pin_href = CAMERA_PIN_HREF,
|
||||
.pin_pclk = CAMERA_PIN_PCLK,
|
||||
|
||||
//XCLK 20MHz or 10MHz for OV2640 double FPS (Experimental)
|
||||
.xclk_freq_hz = mclk_freq,
|
||||
.ledc_timer = LEDC_TIMER_0,
|
||||
.ledc_channel = LEDC_CHANNEL_0,
|
||||
|
||||
.pixel_format = pixel_fromat, //YUV422,GRAYSCALE,RGB565,JPEG
|
||||
.frame_size = frame_size, //QQVGA-UXGA Do not use sizes above QVGA when not JPEG
|
||||
|
||||
.jpeg_quality = 9, //0-63 lower number means higher quality
|
||||
.fb_count = frame_buffer_count, //if more than one, i2s runs in continuous mode. Use only with JPEG
|
||||
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
|
||||
.fb_location = CAMERA_FB_IN_PSRAM,
|
||||
};
|
||||
|
||||
// camera init
|
||||
esp_err_t err = esp_camera_init(&camera_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
sensor_t *s = esp_camera_sensor_get();
|
||||
s->set_vflip(s, 1); //flip it back
|
||||
s->set_hmirror(s, 1);
|
||||
s->set_saturation(s, 1);
|
||||
//initial sensors are flipped vertically and colors are a bit saturated
|
||||
if (s->id.PID == OV3660_PID) {
|
||||
s->set_brightness(s, 1); //up the blightness just a bit
|
||||
s->set_saturation(s, -2); //lower the saturation
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static uint32_t camera_test_fps(uint16_t times)
|
||||
{
|
||||
uint32_t image_size = 0;
|
||||
uint32_t ret;
|
||||
ESP_LOGI(TAG, "satrt to test fps");
|
||||
esp_camera_fb_return(esp_camera_fb_get());
|
||||
esp_camera_fb_return(esp_camera_fb_get());
|
||||
|
||||
uint64_t total_time = esp_timer_get_time();
|
||||
for (size_t i = 0; i < times; i++) {
|
||||
camera_fb_t *pic = esp_camera_fb_get();
|
||||
if (NULL == pic) {
|
||||
ESP_LOGW(TAG, "fb get failed");
|
||||
continue;
|
||||
}
|
||||
|
||||
image_size += pic->len;
|
||||
esp_camera_fb_return(pic);
|
||||
}
|
||||
total_time = esp_timer_get_time() - total_time;
|
||||
float fps = times / (total_time / 1000000.0f);
|
||||
ret = image_size / times;
|
||||
ESP_LOGI(TAG, "fps=%f, image_average_size=%u", fps, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int _get_frame(void **buf, size_t *len)
|
||||
{
|
||||
camera_fb_t *image_fb = esp_camera_fb_get();
|
||||
if (!image_fb) {
|
||||
ESP_LOGE(TAG, "Camera capture failed");
|
||||
return -1;
|
||||
} else {
|
||||
// ESP_LOGI(TAG, "len=%d", image_fb->len);
|
||||
*buf = &image_fb->buf;
|
||||
*len = image_fb->len;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _return_frame(void *inbuf)
|
||||
{
|
||||
camera_fb_t *image_fb = __containerof(inbuf, camera_fb_t, buf);
|
||||
esp_camera_fb_return(image_fb);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_FILE_SERVER_ENABLED
|
||||
esp_err_t example_start_file_server(const char *base_path);
|
||||
|
||||
static esp_err_t server_start(void)
|
||||
{
|
||||
/* This helper function configures Wi-Fi */
|
||||
app_wifi_main();
|
||||
|
||||
/* Start the file server */
|
||||
ESP_ERROR_CHECK(example_start_file_server(SD_CARD_MOUNT_POINT));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
void app_main()
|
||||
{
|
||||
const char *video_file = SD_CARD_MOUNT_POINT"/recorde.avi";
|
||||
int ret = camera_init(20000000, PIXFORMAT_JPEG, EXAMPLE_SENSOR_FRAME_SIZE, EXAMPLE_FB_COUNT);
|
||||
if (ret != 0) {
|
||||
ESP_LOGE(TAG, "camera init fail");
|
||||
} else {
|
||||
camera_test_fps(16);
|
||||
|
||||
ESP_ERROR_CHECK(fm_sdcard_init()); /* Initialize file storage */
|
||||
|
||||
avi_recorder_start(video_file, _get_frame, _return_frame, resolution[EXAMPLE_SENSOR_FRAME_SIZE].width, resolution[EXAMPLE_SENSOR_FRAME_SIZE].height, 10 * 2, 1);
|
||||
}
|
||||
|
||||
#if CONFIG_EXAMPLE_FILE_SERVER_ENABLED
|
||||
server_start();
|
||||
#else
|
||||
avi_recorder_stop();
|
||||
fm_unmount_sdcard();
|
||||
#endif
|
||||
}
|
||||
203
examples/camera/video_recorder/main/app_wifi.c
Normal file
203
examples/camera/video_recorder/main/app_wifi.c
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright 2021-2022 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/event_groups.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "lwip/err.h"
|
||||
#include "lwip/sys.h"
|
||||
|
||||
/* The examples use WiFi configuration that you can set via 'make menuconfig'.
|
||||
|
||||
If you'd rather not, just change the below entries to strings with
|
||||
the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid"
|
||||
*/
|
||||
#define EXAMPLE_ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
|
||||
#define EXAMPLE_ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
|
||||
#define EXAMPLE_ESP_MAXIMUM_RETRY CONFIG_ESP_MAXIMUM_RETRY
|
||||
#define EXAMPLE_ESP_WIFI_AP_SSID CONFIG_ESP_WIFI_AP_SSID
|
||||
#define EXAMPLE_ESP_WIFI_AP_PASS CONFIG_ESP_WIFI_AP_PASSWORD
|
||||
#define EXAMPLE_MAX_STA_CONN CONFIG_MAX_STA_CONN
|
||||
#define EXAMPLE_IP_ADDR CONFIG_SERVER_IP
|
||||
#define EXAMPLE_ESP_WIFI_AP_CHANNEL CONFIG_ESP_WIFI_AP_CHANNEL
|
||||
|
||||
static const char *TAG = "camera wifi";
|
||||
|
||||
static int s_retry_num = 0;
|
||||
/* FreeRTOS event group to signal when we are connected*/
|
||||
static EventGroupHandle_t s_wifi_event_group = NULL;
|
||||
|
||||
/* The event group allows multiple bits for each event, but we only care about two events:
|
||||
* - we are connected to the AP with an IP
|
||||
* - we failed to connect after the maximum amount of retries */
|
||||
#define WIFI_CONNECTED_BIT BIT0
|
||||
#define WIFI_FAIL_BIT BIT1
|
||||
|
||||
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data)
|
||||
{
|
||||
/* AP mode */
|
||||
if (event_id == WIFI_EVENT_AP_STACONNECTED) {
|
||||
wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" join, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
|
||||
wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
|
||||
ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d",
|
||||
MAC2STR(event->mac), event->aid);
|
||||
}
|
||||
/* Sta mode */
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||
esp_wifi_connect();
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) {
|
||||
esp_wifi_connect();
|
||||
s_retry_num++;
|
||||
ESP_LOGI(TAG, "retry to connect to the AP");
|
||||
} else {
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
|
||||
}
|
||||
ESP_LOGI(TAG, "connect to the AP fail");
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
|
||||
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
|
||||
s_retry_num = 0;
|
||||
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
static void wifi_init_softap(esp_netif_t *netif)
|
||||
{
|
||||
if (strcmp(EXAMPLE_IP_ADDR, "192.168.4.1")) {
|
||||
int a, b, c, d;
|
||||
sscanf(EXAMPLE_IP_ADDR, "%d.%d.%d.%d", &a, &b, &c, &d);
|
||||
esp_netif_ip_info_t ip_info;
|
||||
IP4_ADDR(&ip_info.ip, a, b, c, d);
|
||||
IP4_ADDR(&ip_info.gw, a, b, c, d);
|
||||
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
|
||||
ESP_ERROR_CHECK(esp_netif_dhcps_stop(netif));
|
||||
ESP_ERROR_CHECK(esp_netif_set_ip_info(netif, &ip_info));
|
||||
ESP_ERROR_CHECK(esp_netif_dhcps_start(netif));
|
||||
}
|
||||
wifi_config_t wifi_config;
|
||||
memset(&wifi_config, 0, sizeof(wifi_config_t));
|
||||
snprintf((char *)wifi_config.ap.ssid, 32, "%s", EXAMPLE_ESP_WIFI_AP_SSID);
|
||||
wifi_config.ap.ssid_len = strlen((char *)wifi_config.ap.ssid);
|
||||
snprintf((char *)wifi_config.ap.password, 64, "%s", EXAMPLE_ESP_WIFI_AP_PASS);
|
||||
wifi_config.ap.max_connection = EXAMPLE_MAX_STA_CONN;
|
||||
wifi_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
if (strlen(EXAMPLE_ESP_WIFI_AP_PASS) == 0) {
|
||||
wifi_config.ap.authmode = WIFI_AUTH_OPEN;
|
||||
}
|
||||
if (strlen(EXAMPLE_ESP_WIFI_AP_CHANNEL)) {
|
||||
int channel;
|
||||
sscanf(EXAMPLE_ESP_WIFI_AP_CHANNEL, "%d", &channel);
|
||||
wifi_config.ap.channel = channel;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config));
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_softap finished.SSID:%s password:%s",
|
||||
EXAMPLE_ESP_WIFI_AP_SSID, EXAMPLE_ESP_WIFI_AP_PASS);
|
||||
}
|
||||
|
||||
static void wifi_init_sta()
|
||||
{
|
||||
wifi_config_t wifi_config;
|
||||
memset(&wifi_config, 0, sizeof(wifi_config_t));
|
||||
snprintf((char *)wifi_config.sta.ssid, 32, "%s", EXAMPLE_ESP_WIFI_SSID);
|
||||
snprintf((char *)wifi_config.sta.password, 64, "%s", EXAMPLE_ESP_WIFI_PASS);
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config));
|
||||
|
||||
ESP_LOGI(TAG, "wifi_init_sta finished.");
|
||||
ESP_LOGI(TAG, "connect to ap SSID:%s password:%s",
|
||||
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
|
||||
}
|
||||
|
||||
void app_wifi_main()
|
||||
{
|
||||
esp_err_t ret = nvs_flash_init();
|
||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
ret = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(ret);
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
wifi_mode_t mode = WIFI_MODE_NULL;
|
||||
|
||||
if (strlen(EXAMPLE_ESP_WIFI_AP_SSID) && strlen(EXAMPLE_ESP_WIFI_SSID)) {
|
||||
mode = WIFI_MODE_APSTA;
|
||||
} else if (strlen(EXAMPLE_ESP_WIFI_AP_SSID)) {
|
||||
mode = WIFI_MODE_AP;
|
||||
} else if (strlen(EXAMPLE_ESP_WIFI_SSID)) {
|
||||
mode = WIFI_MODE_STA;
|
||||
}
|
||||
|
||||
if (mode == WIFI_MODE_NULL) {
|
||||
ESP_LOGW(TAG, "Neither AP or STA have been configured. WiFi will be off.");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_ERROR_CHECK(esp_netif_init());
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
s_wifi_event_group = xEventGroupCreate();
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
|
||||
ESP_EVENT_ANY_ID,
|
||||
&wifi_event_handler,
|
||||
NULL,
|
||||
NULL));
|
||||
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
|
||||
IP_EVENT_STA_GOT_IP,
|
||||
&wifi_event_handler,
|
||||
NULL,
|
||||
NULL));
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(mode));
|
||||
|
||||
if (mode & WIFI_MODE_AP) {
|
||||
esp_netif_t *ap_netif = esp_netif_create_default_wifi_ap();
|
||||
wifi_init_softap(ap_netif);
|
||||
}
|
||||
|
||||
if (mode & WIFI_MODE_STA) {
|
||||
esp_netif_create_default_wifi_sta();
|
||||
wifi_init_sta();
|
||||
}
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||
ESP_LOGI(TAG, "wifi init finished.");
|
||||
|
||||
if (mode & WIFI_MODE_STA) {
|
||||
xEventGroupWaitBits(s_wifi_event_group,
|
||||
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
|
||||
pdFALSE,
|
||||
pdFALSE,
|
||||
portMAX_DELAY);
|
||||
}
|
||||
vEventGroupDelete(s_wifi_event_group);
|
||||
s_wifi_event_group = NULL;
|
||||
}
|
||||
37
examples/camera/video_recorder/main/app_wifi.h
Normal file
37
examples/camera/video_recorder/main/app_wifi.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2022 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on ESPRESSIF SYSTEMS products only, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
#ifndef _APP_WIFI_H_
|
||||
#define _APP_WIFI_H_
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void app_wifi_main();
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
BIN
examples/camera/video_recorder/main/favicon.ico
Normal file
BIN
examples/camera/video_recorder/main/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
423
examples/camera/video_recorder/main/file_manager.c
Normal file
423
examples/camera/video_recorder/main/file_manager.c
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
// Copyright 2020-2022 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <ctype.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_vfs_fat.h"
|
||||
#include "driver/sdspi_host.h"
|
||||
#include "driver/spi_common.h"
|
||||
#include "sdmmc_cmd.h"
|
||||
#include "file_manager.h"
|
||||
|
||||
static const char *TAG = "file manager";
|
||||
|
||||
#define FLN_MAX CONFIG_FATFS_MAX_LFN
|
||||
|
||||
#ifndef CONFIG_EXAMPLE_USE_SDMMC_HOST
|
||||
#define USE_SPI_MODE
|
||||
#else
|
||||
#if defined CONFIG_IDF_TARGET_ESP32 || defined CONFIG_IDF_TARGET_ESP32S3
|
||||
#include "driver/sdmmc_host.h"
|
||||
static sdmmc_card_t *mount_card = NULL;
|
||||
#endif // define USE_SPI_MODE /* To enable SPI mode, uncomment this line*/
|
||||
#endif // end define CONFIG_EXAMPLE_USE_SDMMC_HOST
|
||||
|
||||
// ESP32-S2 doesn't have an SD Host peripheral, always use SPI:
|
||||
#ifdef CONFIG_IDF_TARGET_ESP32S2
|
||||
#ifndef USE_SPI_MODE
|
||||
#define USE_SPI_MODE
|
||||
#endif // USE_SPI_MODE
|
||||
// on ESP32-S2, DMA channel must be the same as host id
|
||||
#define SPI_DMA_CHAN host.slot
|
||||
#endif //CONFIG_IDF_TARGET_ESP32S2
|
||||
|
||||
// DMA channel to be used by the SPI peripheral
|
||||
#ifndef SPI_DMA_CHAN
|
||||
#define SPI_DMA_CHAN 1
|
||||
#endif //SPI_DMA_CHAN
|
||||
|
||||
// When testing SD and SPI modes, keep in mind that once the card has been
|
||||
// initialized in SPI mode, it can not be reinitialized in SD mode without
|
||||
// toggling power to the card.
|
||||
|
||||
#ifdef USE_SPI_MODE
|
||||
// Pin mapping when using SPI mode.
|
||||
// With this mapping, SD card can be used both in SPI and 1-line SD mode.
|
||||
// Note that a pull-up on CS line is required in SD mode.
|
||||
#define PIN_NUM_MISO 2
|
||||
#define PIN_NUM_MOSI 15
|
||||
#define PIN_NUM_CLK 14
|
||||
#define PIN_NUM_CS 13
|
||||
#endif //USE_SPI_MODE
|
||||
|
||||
esp_err_t fm_sdcard_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing SD card");
|
||||
|
||||
#ifndef USE_SPI_MODE
|
||||
ESP_LOGI(TAG, "Using SDMMC peripheral");
|
||||
sdmmc_host_t host = SDMMC_HOST_DEFAULT();
|
||||
// host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;
|
||||
|
||||
// This initializes the slot without card detect (CD) and write protect (WP) signals.
|
||||
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
|
||||
sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();
|
||||
// Enable internal pullups on enabled pins. The internal pullups
|
||||
// are insufficient however, please make sure 10k external pullups are
|
||||
// connected on the bus. This is for debug / example purpose only.
|
||||
slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;
|
||||
#ifdef CONFIG_CAMERA_MODULE_ESP_S3_EYE
|
||||
// To use 1-line SD mode, change this to 1:
|
||||
slot_config.width = 1;
|
||||
#ifdef SOC_SDMMC_USE_GPIO_MATRIX
|
||||
// On chips where the GPIOs used for SD card can be configured, set them in
|
||||
// the slot_config structure:
|
||||
slot_config.clk = 39;
|
||||
slot_config.cmd = 38;
|
||||
slot_config.d0 = 40;
|
||||
slot_config.d1 = -1;
|
||||
slot_config.d2 = -1;
|
||||
slot_config.d3 = -1;
|
||||
#endif // end SOC_SDMMC_USE_GPIO_MATRIX
|
||||
|
||||
#else // if not s3-eye model
|
||||
// To use 1-line SD mode, uncomment the following line:
|
||||
slot_config.width = 4;
|
||||
/* open SD card in MMC 1 bit mode
|
||||
MMC4 MMC1 ESP32
|
||||
D2 12
|
||||
D3 CS 13
|
||||
CMD MOSI 15
|
||||
CLK SCK 14
|
||||
D0 MISO 2
|
||||
D1 4
|
||||
*/
|
||||
#ifdef SOC_SDMMC_USE_GPIO_MATRIX
|
||||
slot_config.clk = GPIO_NUM_14;
|
||||
slot_config.cmd = GPIO_NUM_15;
|
||||
slot_config.d0 = GPIO_NUM_2;
|
||||
slot_config.d1 = GPIO_NUM_4;
|
||||
slot_config.d2 = GPIO_NUM_12;
|
||||
slot_config.d3 = GPIO_NUM_13;
|
||||
#endif
|
||||
// GPIOs 15, 2, 4, 12, 13 should have external 10k pull-ups.
|
||||
// Internal pull-ups are not sufficient. However, enabling internal pull-ups
|
||||
// does make a difference some boards, so we do that here.
|
||||
gpio_set_pull_mode(15, GPIO_PULLUP_ONLY); // CMD, needed in 4- and 1- line modes
|
||||
gpio_set_pull_mode(2, GPIO_PULLUP_ONLY); // D0, needed in 4- and 1-line modes
|
||||
gpio_set_pull_mode(4, GPIO_PULLUP_ONLY); // D1, needed in 4-line mode only
|
||||
gpio_set_pull_mode(12, GPIO_PULLUP_ONLY); // D2, needed in 4-line mode only
|
||||
gpio_set_pull_mode(13, GPIO_PULLUP_ONLY); // D3, needed in 4- and 1-line modes
|
||||
#endif // end CONFIG_CAMERA_MODULE_ESP_S3_EYE
|
||||
#else // if use SPI modes
|
||||
ESP_LOGI(TAG, "Using SPI peripheral");
|
||||
|
||||
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
|
||||
sdspi_slot_config_t slot_config = SDSPI_SLOT_CONFIG_DEFAULT();
|
||||
slot_config.gpio_miso = PIN_NUM_MISO;
|
||||
slot_config.gpio_mosi = PIN_NUM_MOSI;
|
||||
slot_config.gpio_sck = PIN_NUM_CLK;
|
||||
slot_config.gpio_cs = PIN_NUM_CS;
|
||||
// This initializes the slot without card detect (CD) and write protect (WP) signals.
|
||||
// Modify slot_config.gpio_cd and slot_config.gpio_wp if your board has these signals.
|
||||
#endif // end USE_SPI_MODE
|
||||
|
||||
// Options for mounting the filesystem.
|
||||
// If format_if_mount_failed is set to true, SD card will be partitioned and
|
||||
// formatted in case when mounting fails.
|
||||
esp_vfs_fat_sdmmc_mount_config_t mount_config = {
|
||||
#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.format_if_mount_failed = true,
|
||||
#else
|
||||
.format_if_mount_failed = false,
|
||||
#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED
|
||||
.max_files = 10,
|
||||
.allocation_unit_size = 32 * 1024,
|
||||
};
|
||||
|
||||
// Use settings defined above to initialize SD card and mount FAT filesystem.
|
||||
// Note: esp_vfs_fat_sdmmc_mount is an all-in-one convenience function.
|
||||
// Please check its source code and implement error recovery when developing
|
||||
// production applications.
|
||||
sdmmc_card_t *card;
|
||||
esp_err_t ret = esp_vfs_fat_sdmmc_mount(SD_CARD_MOUNT_POINT, &host, &slot_config, &mount_config, &card);
|
||||
mount_card = card;
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Failed to mount filesystem. "
|
||||
"If you want the card to be formatted, set format_if_mount_failed = true.");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize the card (%s). "
|
||||
"Make sure SD card lines have pull-up resistors in place.", esp_err_to_name(ret));
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Card has been initialized, print its properties
|
||||
sdmmc_card_print_info(stdout, card);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t fm_unmount_sdcard(void)
|
||||
{
|
||||
#ifdef USE_SPI_MODE
|
||||
esp_err_t err = esp_vfs_fat_sdcard_unmount(SD_CARD_MOUNT_POINT, mount_card);
|
||||
#else
|
||||
esp_err_t err = esp_vfs_fat_sdmmc_unmount();
|
||||
#endif
|
||||
ESP_ERROR_CHECK(err);
|
||||
#ifdef USE_SPI_MODE
|
||||
sdmmc_host_t host = SDSPI_HOST_DEFAULT();
|
||||
err = spi_bus_free(host.slot);
|
||||
#endif
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
esp_err_t fm_spiffs_init(void)
|
||||
{
|
||||
esp_err_t ret;
|
||||
ESP_LOGI(TAG, "Initializing SPIFFS");
|
||||
|
||||
esp_vfs_spiffs_conf_t conf = {
|
||||
.base_path = SPIFFS_MOUNT_POINT,
|
||||
.partition_label = NULL,
|
||||
.max_files = 5, // This decides the maximum number of files that can be created on the storage
|
||||
.format_if_mount_failed = true
|
||||
};
|
||||
|
||||
ret = esp_vfs_spiffs_register(&conf);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_FAIL) {
|
||||
ESP_LOGE(TAG, "Failed to mount or format filesystem");
|
||||
} else if (ret == ESP_ERR_NOT_FOUND) {
|
||||
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
|
||||
}
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
size_t total = 0, used = 0;
|
||||
ret = esp_spiffs_info(NULL, &total, &used);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void TraverseDir(char *direntName, int level, int indent)
|
||||
{
|
||||
DIR *p_dir = NULL;
|
||||
struct dirent *p_dirent = NULL;
|
||||
|
||||
p_dir = opendir(direntName);
|
||||
|
||||
if (p_dir == NULL) {
|
||||
printf("opendir error\n");
|
||||
return;
|
||||
}
|
||||
|
||||
while ((p_dirent = readdir(p_dir)) != NULL) {
|
||||
char *backupDirName = NULL;
|
||||
|
||||
if (p_dirent->d_name[0] == '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < indent; i++) {
|
||||
// printf("|");
|
||||
printf(" ");
|
||||
}
|
||||
|
||||
printf("|--- %s", p_dirent->d_name);
|
||||
|
||||
/* Itme is a file */
|
||||
if (p_dirent->d_type == DT_REG) {
|
||||
int curDirentNameLen = strlen(direntName) + strlen(p_dirent->d_name) + 2;
|
||||
|
||||
//prepare next path
|
||||
backupDirName = (char *)malloc(curDirentNameLen);
|
||||
struct stat *st = NULL;
|
||||
st = malloc(sizeof(struct stat));
|
||||
|
||||
if (NULL == backupDirName || NULL == st) {
|
||||
goto _err;
|
||||
}
|
||||
|
||||
memset(backupDirName, 0, curDirentNameLen);
|
||||
memcpy(backupDirName, direntName, strlen(direntName));
|
||||
|
||||
strcat(backupDirName, "/");
|
||||
strcat(backupDirName, p_dirent->d_name);
|
||||
|
||||
int statok = stat(backupDirName, st);
|
||||
|
||||
if (0 == statok) {
|
||||
printf("[%dB]\n", (int)st->st_size);
|
||||
} else {
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
free(backupDirName);
|
||||
backupDirName = NULL;
|
||||
} else {
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
/* Itme is a directory */
|
||||
if (p_dirent->d_type == DT_DIR) {
|
||||
int curDirentNameLen = strlen(direntName) + strlen(p_dirent->d_name) + 2;
|
||||
|
||||
//prepare next path
|
||||
backupDirName = (char *)malloc(curDirentNameLen);
|
||||
|
||||
if (NULL == backupDirName) {
|
||||
goto _err;
|
||||
}
|
||||
|
||||
memset(backupDirName, 0, curDirentNameLen);
|
||||
memcpy(backupDirName, direntName, curDirentNameLen);
|
||||
|
||||
strcat(backupDirName, "/");
|
||||
strcat(backupDirName, p_dirent->d_name);
|
||||
|
||||
if (level > 0) {
|
||||
TraverseDir(backupDirName, level - 1, indent + 1);
|
||||
}
|
||||
|
||||
free(backupDirName);
|
||||
backupDirName = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
_err:
|
||||
closedir(p_dir);
|
||||
}
|
||||
|
||||
void fm_print_dir(char *direntName, int level)
|
||||
{
|
||||
printf("Traverse directory %s\n", direntName);
|
||||
TraverseDir(direntName, level, 0);
|
||||
printf("\r\n");
|
||||
}
|
||||
|
||||
const char *fm_get_basepath(void)
|
||||
{
|
||||
return SPIFFS_MOUNT_POINT;
|
||||
}
|
||||
|
||||
esp_err_t fm_file_table_create(char ***list_out, uint16_t *files_number, const char *filter_suffix)
|
||||
{
|
||||
DIR *p_dir = NULL;
|
||||
struct dirent *p_dirent = NULL;
|
||||
|
||||
p_dir = opendir(SPIFFS_MOUNT_POINT);
|
||||
|
||||
if (p_dir == NULL) {
|
||||
ESP_LOGE(TAG, "opendir error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint16_t f_num = 0;
|
||||
while ((p_dirent = readdir(p_dir)) != NULL) {
|
||||
if (p_dirent->d_type == DT_REG) {
|
||||
f_num++;
|
||||
}
|
||||
}
|
||||
|
||||
rewinddir(p_dir);
|
||||
|
||||
*list_out = calloc(f_num, sizeof(char *));
|
||||
if (NULL == (*list_out)) {
|
||||
goto _err;
|
||||
}
|
||||
for (size_t i = 0; i < f_num; i++) {
|
||||
(*list_out)[i] = malloc(FLN_MAX);
|
||||
if (NULL == (*list_out)[i]) {
|
||||
ESP_LOGE(TAG, "malloc failed at %d", i);
|
||||
fm_file_table_free(list_out, f_num);
|
||||
goto _err;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t index = 0;
|
||||
while ((p_dirent = readdir(p_dir)) != NULL) {
|
||||
if (p_dirent->d_type == DT_REG) {
|
||||
if (NULL != filter_suffix) {
|
||||
if (strstr(p_dirent->d_name, filter_suffix)) {
|
||||
strncpy((*list_out)[index], p_dirent->d_name, FLN_MAX - 1);
|
||||
(*list_out)[index][FLN_MAX - 1] = '\0';
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
strncpy((*list_out)[index], p_dirent->d_name, FLN_MAX - 1);
|
||||
(*list_out)[index][FLN_MAX - 1] = '\0';
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
(*files_number) = index;
|
||||
|
||||
closedir(p_dir);
|
||||
return ESP_OK;
|
||||
_err:
|
||||
closedir(p_dir);
|
||||
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t fm_file_table_free(char ***list, uint16_t files_number)
|
||||
{
|
||||
for (size_t i = 0; i < files_number; i++) {
|
||||
free((*list)[i]);
|
||||
}
|
||||
free((*list));
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
const char *fm_get_filename(const char *file)
|
||||
{
|
||||
const char *p = file + strlen(file);
|
||||
while (p > file) {
|
||||
if ('/' == *p) {
|
||||
return (p + 1);
|
||||
}
|
||||
p--;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
size_t fm_get_file_size(const char *filepath)
|
||||
{
|
||||
struct stat siz = {0};
|
||||
stat(filepath, &siz);
|
||||
return siz.st_size;
|
||||
}
|
||||
44
examples/camera/video_recorder/main/file_manager.h
Normal file
44
examples/camera/video_recorder/main/file_manager.h
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2020-2022 Espressif Systems (Shanghai) PTE LTD
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef _IOT_FILE_MANAGER_H_
|
||||
#define _IOT_FILE_MANAGER_H_
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "esp_vfs.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SPIFFS_MOUNT_POINT "/spiffs"
|
||||
#define SD_CARD_MOUNT_POINT "/sdcard"
|
||||
|
||||
esp_err_t fm_sdcard_init(void);
|
||||
esp_err_t fm_spiffs_init(void);
|
||||
void fm_print_dir(char *direntName, int level);
|
||||
const char *fm_get_basepath(void);
|
||||
const char *fm_get_filename(const char *file);
|
||||
size_t fm_get_file_size(const char *filepath);
|
||||
esp_err_t fm_file_table_create(char ***list_out, uint16_t *files_number, const char *filter_suffix);
|
||||
esp_err_t fm_file_table_free(char ***list,uint16_t files_number);
|
||||
esp_err_t fm_unmount_sdcard(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
506
examples/camera/video_recorder/main/file_server.c
Normal file
506
examples/camera/video_recorder/main/file_server.c
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
/* HTTP File Server Example
|
||||
|
||||
This example code is in the Public Domain (or CC0 licensed, at your option.)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, this
|
||||
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
||||
CONDITIONS OF ANY KIND, either express or implied.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
#include "esp_vfs.h"
|
||||
#include "esp_spiffs.h"
|
||||
#include "esp_http_server.h"
|
||||
|
||||
/* Max length a file path can have on storage */
|
||||
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN)
|
||||
|
||||
/* Max size of an individual file. Make sure this
|
||||
* value is same as that set in upload_script.html */
|
||||
#define MAX_FILE_SIZE (200*1024) // 200 KB
|
||||
#define MAX_FILE_SIZE_STR "200KB"
|
||||
|
||||
/* Scratch buffer size */
|
||||
#define SCRATCH_BUFSIZE 8192
|
||||
|
||||
struct file_server_data {
|
||||
/* Base path of file storage */
|
||||
char base_path[ESP_VFS_PATH_MAX + 1];
|
||||
|
||||
/* Scratch buffer for temporary storage during file transfer */
|
||||
char scratch[SCRATCH_BUFSIZE];
|
||||
};
|
||||
|
||||
static const char *TAG = "file_server";
|
||||
|
||||
/* Handler to redirect incoming GET request for /index.html to /
|
||||
* This can be overridden by uploading file with same name */
|
||||
static esp_err_t index_html_get_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_status(req, "307 Temporary Redirect");
|
||||
httpd_resp_set_hdr(req, "Location", "/");
|
||||
httpd_resp_send(req, NULL, 0); // Response body can be empty
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Handler to respond with an icon file embedded in flash.
|
||||
* Browsers expect to GET website icon at URI /favicon.ico.
|
||||
* This can be overridden by uploading file with same name */
|
||||
static esp_err_t favicon_get_handler(httpd_req_t *req)
|
||||
{
|
||||
extern const unsigned char favicon_ico_start[] asm("_binary_favicon_ico_start");
|
||||
extern const unsigned char favicon_ico_end[] asm("_binary_favicon_ico_end");
|
||||
const size_t favicon_ico_size = (favicon_ico_end - favicon_ico_start);
|
||||
httpd_resp_set_type(req, "image/x-icon");
|
||||
httpd_resp_send(req, (const char *)favicon_ico_start, favicon_ico_size);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Send HTTP response with a run-time generated html consisting of
|
||||
* a list of all files and folders under the requested path.
|
||||
* In case of SPIFFS this returns empty list when path is any
|
||||
* string other than '/', since SPIFFS doesn't support directories */
|
||||
static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath)
|
||||
{
|
||||
char entrypath[FILE_PATH_MAX];
|
||||
char entrysize[16];
|
||||
const char *entrytype;
|
||||
|
||||
struct dirent *entry;
|
||||
struct stat entry_stat;
|
||||
|
||||
DIR *dir = opendir(dirpath);
|
||||
const size_t dirpath_len = strlen(dirpath);
|
||||
|
||||
/* Retrieve the base path of file storage to construct the full path */
|
||||
strlcpy(entrypath, dirpath, sizeof(entrypath));
|
||||
|
||||
if (!dir) {
|
||||
ESP_LOGE(TAG, "Failed to stat dir : %s", dirpath);
|
||||
/* Respond with 404 Not Found */
|
||||
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Directory does not exist");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Send HTML file header */
|
||||
httpd_resp_sendstr_chunk(req, "<!DOCTYPE html><html><body>");
|
||||
|
||||
/* Get handle to embedded file upload script */
|
||||
extern const unsigned char upload_script_start[] asm("_binary_upload_script_html_start");
|
||||
extern const unsigned char upload_script_end[] asm("_binary_upload_script_html_end");
|
||||
const size_t upload_script_size = (upload_script_end - upload_script_start);
|
||||
|
||||
/* Add file upload form and script which on execution sends a POST request to /upload */
|
||||
httpd_resp_send_chunk(req, (const char *)upload_script_start, upload_script_size);
|
||||
|
||||
/* Send file-list table definition and column labels */
|
||||
httpd_resp_sendstr_chunk(req,
|
||||
"<table class=\"fixed\" border=\"1\">"
|
||||
"<col width=\"800px\" /><col width=\"300px\" /><col width=\"300px\" /><col width=\"100px\" />"
|
||||
"<thead><tr><th>Name</th><th>Type</th><th>Size (Bytes)</th><th>Delete</th></tr></thead>"
|
||||
"<tbody>");
|
||||
|
||||
/* Iterate over all files / folders and fetch their names and sizes */
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
|
||||
|
||||
strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len);
|
||||
if (stat(entrypath, &entry_stat) == -1) {
|
||||
ESP_LOGE(TAG, "Failed to stat %s : %s", entrytype, entry->d_name);
|
||||
continue;
|
||||
}
|
||||
sprintf(entrysize, "%ld", entry_stat.st_size);
|
||||
ESP_LOGI(TAG, "Found %s : %s (%s bytes)", entrytype, entry->d_name, entrysize);
|
||||
|
||||
/* Send chunk of HTML file containing table entries with file name and size */
|
||||
httpd_resp_sendstr_chunk(req, "<tr><td><a href=\"");
|
||||
httpd_resp_sendstr_chunk(req, req->uri);
|
||||
httpd_resp_sendstr_chunk(req, entry->d_name);
|
||||
if (entry->d_type == DT_DIR) {
|
||||
httpd_resp_sendstr_chunk(req, "/");
|
||||
}
|
||||
httpd_resp_sendstr_chunk(req, "\">");
|
||||
httpd_resp_sendstr_chunk(req, entry->d_name);
|
||||
httpd_resp_sendstr_chunk(req, "</a></td><td>");
|
||||
httpd_resp_sendstr_chunk(req, entrytype);
|
||||
httpd_resp_sendstr_chunk(req, "</td><td>");
|
||||
httpd_resp_sendstr_chunk(req, entrysize);
|
||||
httpd_resp_sendstr_chunk(req, "</td><td>");
|
||||
httpd_resp_sendstr_chunk(req, "<form method=\"post\" action=\"/delete");
|
||||
httpd_resp_sendstr_chunk(req, req->uri);
|
||||
httpd_resp_sendstr_chunk(req, entry->d_name);
|
||||
httpd_resp_sendstr_chunk(req, "\"><button type=\"submit\">Delete</button></form>");
|
||||
httpd_resp_sendstr_chunk(req, "</td></tr>\n");
|
||||
}
|
||||
closedir(dir);
|
||||
|
||||
/* Finish the file list table */
|
||||
httpd_resp_sendstr_chunk(req, "</tbody></table>");
|
||||
|
||||
/* Send remaining chunk of HTML file to complete it */
|
||||
httpd_resp_sendstr_chunk(req, "</body></html>");
|
||||
|
||||
/* Send empty chunk to signal HTTP response completion */
|
||||
httpd_resp_sendstr_chunk(req, NULL);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#define IS_FILE_EXT(filename, ext) \
|
||||
(strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)
|
||||
|
||||
/* Set HTTP response content type according to file extension */
|
||||
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename)
|
||||
{
|
||||
if (IS_FILE_EXT(filename, ".pdf")) {
|
||||
return httpd_resp_set_type(req, "application/pdf");
|
||||
} else if (IS_FILE_EXT(filename, ".html")) {
|
||||
return httpd_resp_set_type(req, "text/html");
|
||||
} else if (IS_FILE_EXT(filename, ".jpeg")) {
|
||||
return httpd_resp_set_type(req, "image/jpeg");
|
||||
} else if (IS_FILE_EXT(filename, ".ico")) {
|
||||
return httpd_resp_set_type(req, "image/x-icon");
|
||||
}
|
||||
/* This is a limited set only */
|
||||
/* For any other type always set as plain text */
|
||||
return httpd_resp_set_type(req, "text/plain");
|
||||
}
|
||||
|
||||
/* Copies the full path into destination buffer and returns
|
||||
* pointer to path (skipping the preceding base path) */
|
||||
static const char* get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize)
|
||||
{
|
||||
const size_t base_pathlen = strlen(base_path);
|
||||
size_t pathlen = strlen(uri);
|
||||
|
||||
const char *quest = strchr(uri, '?');
|
||||
if (quest) {
|
||||
pathlen = MIN(pathlen, quest - uri);
|
||||
}
|
||||
const char *hash = strchr(uri, '#');
|
||||
if (hash) {
|
||||
pathlen = MIN(pathlen, hash - uri);
|
||||
}
|
||||
|
||||
if (base_pathlen + pathlen + 1 > destsize) {
|
||||
/* Full path string won't fit into destination buffer */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Construct full path (base + path) */
|
||||
strcpy(dest, base_path);
|
||||
strlcpy(dest + base_pathlen, uri, pathlen + 1);
|
||||
|
||||
/* Return pointer to path, skipping the base */
|
||||
return dest + base_pathlen;
|
||||
}
|
||||
|
||||
/* Handler to download a file kept on the server */
|
||||
static esp_err_t download_get_handler(httpd_req_t *req)
|
||||
{
|
||||
char filepath[FILE_PATH_MAX];
|
||||
FILE *fd = NULL;
|
||||
struct stat file_stat;
|
||||
|
||||
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
|
||||
req->uri, sizeof(filepath));
|
||||
if (!filename) {
|
||||
ESP_LOGE(TAG, "Filename is too long");
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* If name has trailing '/', respond with directory contents */
|
||||
if (filename[strlen(filename) - 1] == '/') {
|
||||
return http_resp_dir_html(req, filepath);
|
||||
}
|
||||
|
||||
if (stat(filepath, &file_stat) == -1) {
|
||||
/* If file not present on SPIFFS check if URI
|
||||
* corresponds to one of the hardcoded paths */
|
||||
if (strcmp(filename, "/index.html") == 0) {
|
||||
return index_html_get_handler(req);
|
||||
} else if (strcmp(filename, "/favicon.ico") == 0) {
|
||||
return favicon_get_handler(req);
|
||||
}
|
||||
ESP_LOGE(TAG, "Failed to stat file : %s", filepath);
|
||||
/* Respond with 404 Not Found */
|
||||
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
fd = fopen(filepath, "r");
|
||||
if (!fd) {
|
||||
ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size);
|
||||
set_content_type_from_file(req, filename);
|
||||
|
||||
/* Retrieve the pointer to scratch buffer for temporary storage */
|
||||
char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
|
||||
size_t chunksize;
|
||||
do {
|
||||
/* Read file in chunks into the scratch buffer */
|
||||
chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);
|
||||
|
||||
if (chunksize > 0) {
|
||||
/* Send the buffer contents as HTTP response chunk */
|
||||
if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
|
||||
fclose(fd);
|
||||
ESP_LOGE(TAG, "File sending failed!");
|
||||
/* Abort sending file */
|
||||
httpd_resp_sendstr_chunk(req, NULL);
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Keep looping till the whole file is sent */
|
||||
} while (chunksize != 0);
|
||||
|
||||
/* Close file after sending complete */
|
||||
fclose(fd);
|
||||
ESP_LOGI(TAG, "File sending complete");
|
||||
|
||||
/* Respond with an empty chunk to signal HTTP response completion */
|
||||
#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
|
||||
httpd_resp_set_hdr(req, "Connection", "close");
|
||||
#endif
|
||||
httpd_resp_send_chunk(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Handler to upload a file onto the server */
|
||||
static esp_err_t upload_post_handler(httpd_req_t *req)
|
||||
{
|
||||
char filepath[FILE_PATH_MAX];
|
||||
FILE *fd = NULL;
|
||||
struct stat file_stat;
|
||||
|
||||
/* Skip leading "/upload" from URI to get filename */
|
||||
/* Note sizeof() counts NULL termination hence the -1 */
|
||||
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
|
||||
req->uri + sizeof("/upload") - 1, sizeof(filepath));
|
||||
if (!filename) {
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Filename cannot have a trailing '/' */
|
||||
if (filename[strlen(filename) - 1] == '/') {
|
||||
ESP_LOGE(TAG, "Invalid filename : %s", filename);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (stat(filepath, &file_stat) == 0) {
|
||||
ESP_LOGE(TAG, "File already exists : %s", filepath);
|
||||
/* Respond with 400 Bad Request */
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File already exists");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* File cannot be larger than a limit */
|
||||
if (req->content_len > MAX_FILE_SIZE) {
|
||||
ESP_LOGE(TAG, "File too large : %d bytes", req->content_len);
|
||||
/* Respond with 400 Bad Request */
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
||||
"File size must be less than "
|
||||
MAX_FILE_SIZE_STR "!");
|
||||
/* Return failure to close underlying connection else the
|
||||
* incoming file content will keep the socket busy */
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
fd = fopen(filepath, "w");
|
||||
if (!fd) {
|
||||
ESP_LOGE(TAG, "Failed to create file : %s", filepath);
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Receiving file : %s...", filename);
|
||||
|
||||
/* Retrieve the pointer to scratch buffer for temporary storage */
|
||||
char *buf = ((struct file_server_data *)req->user_ctx)->scratch;
|
||||
int received;
|
||||
|
||||
/* Content length of the request gives
|
||||
* the size of the file being uploaded */
|
||||
int remaining = req->content_len;
|
||||
|
||||
while (remaining > 0) {
|
||||
|
||||
ESP_LOGI(TAG, "Remaining size : %d", remaining);
|
||||
/* Receive the file part by part into a buffer */
|
||||
if ((received = httpd_req_recv(req, buf, MIN(remaining, SCRATCH_BUFSIZE))) <= 0) {
|
||||
if (received == HTTPD_SOCK_ERR_TIMEOUT) {
|
||||
/* Retry if timeout occurred */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* In case of unrecoverable error,
|
||||
* close and delete the unfinished file*/
|
||||
fclose(fd);
|
||||
unlink(filepath);
|
||||
|
||||
ESP_LOGE(TAG, "File reception failed!");
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Write buffer content to file on storage */
|
||||
if (received && (received != fwrite(buf, 1, received, fd))) {
|
||||
/* Couldn't write everything to file!
|
||||
* Storage may be full? */
|
||||
fclose(fd);
|
||||
unlink(filepath);
|
||||
|
||||
ESP_LOGE(TAG, "File write failed!");
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write file to storage");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Keep track of remaining size of
|
||||
* the file left to be uploaded */
|
||||
remaining -= received;
|
||||
}
|
||||
|
||||
/* Close file upon upload completion */
|
||||
fclose(fd);
|
||||
ESP_LOGI(TAG, "File reception complete");
|
||||
|
||||
/* Redirect onto root to see the updated file list */
|
||||
httpd_resp_set_status(req, "303 See Other");
|
||||
httpd_resp_set_hdr(req, "Location", "/");
|
||||
#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
|
||||
httpd_resp_set_hdr(req, "Connection", "close");
|
||||
#endif
|
||||
httpd_resp_sendstr(req, "File uploaded successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Handler to delete a file from the server */
|
||||
static esp_err_t delete_post_handler(httpd_req_t *req)
|
||||
{
|
||||
char filepath[FILE_PATH_MAX];
|
||||
struct stat file_stat;
|
||||
|
||||
/* Skip leading "/delete" from URI to get filename */
|
||||
/* Note sizeof() counts NULL termination hence the -1 */
|
||||
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
|
||||
req->uri + sizeof("/delete") - 1, sizeof(filepath));
|
||||
if (!filename) {
|
||||
/* Respond with 500 Internal Server Error */
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Filename cannot have a trailing '/' */
|
||||
if (filename[strlen(filename) - 1] == '/') {
|
||||
ESP_LOGE(TAG, "Invalid filename : %s", filename);
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (stat(filepath, &file_stat) == -1) {
|
||||
ESP_LOGE(TAG, "File does not exist : %s", filename);
|
||||
/* Respond with 400 Bad Request */
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File does not exist");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Deleting file : %s", filename);
|
||||
/* Delete file */
|
||||
unlink(filepath);
|
||||
|
||||
/* Redirect onto root to see the updated file list */
|
||||
httpd_resp_set_status(req, "303 See Other");
|
||||
httpd_resp_set_hdr(req, "Location", "/");
|
||||
#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
|
||||
httpd_resp_set_hdr(req, "Connection", "close");
|
||||
#endif
|
||||
httpd_resp_sendstr(req, "File deleted successfully");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Function to start the file server */
|
||||
esp_err_t example_start_file_server(const char *base_path)
|
||||
{
|
||||
static struct file_server_data *server_data = NULL;
|
||||
|
||||
if (server_data) {
|
||||
ESP_LOGE(TAG, "File server already started");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
/* Allocate memory for server data */
|
||||
server_data = calloc(1, sizeof(struct file_server_data));
|
||||
if (!server_data) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for server data");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
strlcpy(server_data->base_path, base_path,
|
||||
sizeof(server_data->base_path));
|
||||
|
||||
httpd_handle_t server = NULL;
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
|
||||
/* Use the URI wildcard matching function in order to
|
||||
* allow the same handler to respond to multiple different
|
||||
* target URIs which match the wildcard scheme */
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
|
||||
ESP_LOGI(TAG, "Starting HTTP Server on port: '%d'", config.server_port);
|
||||
if (httpd_start(&server, &config) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to start file server!");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* URI handler for getting uploaded files */
|
||||
httpd_uri_t file_download = {
|
||||
.uri = "/*", // Match all URIs of type /path/to/file
|
||||
.method = HTTP_GET,
|
||||
.handler = download_get_handler,
|
||||
.user_ctx = server_data // Pass server data as context
|
||||
};
|
||||
httpd_register_uri_handler(server, &file_download);
|
||||
|
||||
/* URI handler for uploading files to server */
|
||||
httpd_uri_t file_upload = {
|
||||
.uri = "/upload/*", // Match all URIs of type /upload/path/to/file
|
||||
.method = HTTP_POST,
|
||||
.handler = upload_post_handler,
|
||||
.user_ctx = server_data // Pass server data as context
|
||||
};
|
||||
httpd_register_uri_handler(server, &file_upload);
|
||||
|
||||
/* URI handler for deleting files from server */
|
||||
httpd_uri_t file_delete = {
|
||||
.uri = "/delete/*", // Match all URIs of type /delete/path/to/file
|
||||
.method = HTTP_POST,
|
||||
.handler = delete_post_handler,
|
||||
.user_ctx = server_data // Pass server data as context
|
||||
};
|
||||
httpd_register_uri_handler(server, &file_delete);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
80
examples/camera/video_recorder/main/upload_script.html
Normal file
80
examples/camera/video_recorder/main/upload_script.html
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<table class="fixed" border="0">
|
||||
<col width="1000px" /><col width="500px" />
|
||||
<tr><td>
|
||||
<h2>ESP32 File Server</h2>
|
||||
</td><td>
|
||||
<table border="0">
|
||||
<tr>
|
||||
<td>
|
||||
<label for="newfile">Upload a file</label>
|
||||
</td>
|
||||
<td colspan="2">
|
||||
<input id="newfile" type="file" onchange="setpath()" style="width:100%;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label for="filepath">Set path on server</label>
|
||||
</td>
|
||||
<td>
|
||||
<input id="filepath" type="text" style="width:100%;">
|
||||
</td>
|
||||
<td>
|
||||
<button id="upload" type="button" onclick="upload()">Upload</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
<script>
|
||||
function setpath() {
|
||||
var default_path = document.getElementById("newfile").files[0].name;
|
||||
document.getElementById("filepath").value = default_path;
|
||||
}
|
||||
function upload() {
|
||||
var filePath = document.getElementById("filepath").value;
|
||||
var upload_path = "/upload/" + filePath;
|
||||
var fileInput = document.getElementById("newfile").files;
|
||||
|
||||
/* Max size of an individual file. Make sure this
|
||||
* value is same as that set in file_server.c */
|
||||
var MAX_FILE_SIZE = 200*1024;
|
||||
var MAX_FILE_SIZE_STR = "200KB";
|
||||
|
||||
if (fileInput.length == 0) {
|
||||
alert("No file selected!");
|
||||
} else if (filePath.length == 0) {
|
||||
alert("File path on server is not set!");
|
||||
} else if (filePath.indexOf(' ') >= 0) {
|
||||
alert("File path on server cannot have spaces!");
|
||||
} else if (filePath[filePath.length-1] == '/') {
|
||||
alert("File name not specified after path!");
|
||||
} else if (fileInput[0].size > 200*1024) {
|
||||
alert("File size must be less than 200KB!");
|
||||
} else {
|
||||
document.getElementById("newfile").disabled = true;
|
||||
document.getElementById("filepath").disabled = true;
|
||||
document.getElementById("upload").disabled = true;
|
||||
|
||||
var file = fileInput[0];
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (xhttp.readyState == 4) {
|
||||
if (xhttp.status == 200) {
|
||||
document.open();
|
||||
document.write(xhttp.responseText);
|
||||
document.close();
|
||||
} else if (xhttp.status == 0) {
|
||||
alert("Server closed the connection abruptly!");
|
||||
location.reload()
|
||||
} else {
|
||||
alert(xhttp.status + " Error!\n" + xhttp.responseText);
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
};
|
||||
xhttp.open("POST", upload_path, true);
|
||||
xhttp.send(file);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
26
examples/camera/video_recorder/sdkconfig.defaults
Normal file
26
examples/camera/video_recorder/sdkconfig.defaults
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
CONFIG_FLASHMODE_QIO=y
|
||||
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
|
||||
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
|
||||
|
||||
#
|
||||
# Camera configuration
|
||||
#
|
||||
CONFIG_ENABLE_TEST_PATTERN=
|
||||
CONFIG_OV2640_SUPPORT=y
|
||||
CONFIG_CAMERA_MODEL_AI_THINKER=y
|
||||
CONFIG_CAMERA_MODEL_WROVER_KIT=n
|
||||
CONFIG_ESP_FACE_DETECT_ENABLED=n
|
||||
CONFIG_ESP_FACE_RECOGNITION_ENABLED=n
|
||||
#
|
||||
# ESP32-specific
|
||||
#
|
||||
CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=500
|
||||
|
||||
CONFIG_FATFS_LFN_STACK=y
|
||||
CONFIG_FATFS_MAX_LFN=127
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
CONFIG_CAMERA_MODULE_ESP_S3_EYE=y
|
||||
|
||||
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
|
||||
CONFIG_SPIRAM_SUPPORT=y
|
||||
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
|
|
@ -143,6 +143,15 @@
|
|||
"esp32s3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "video_recorder",
|
||||
"buildsystem": [
|
||||
"cmake"
|
||||
],
|
||||
"targets": [
|
||||
"esp32s3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "examples",
|
||||
"buildsystem": [
|
||||
|
|
|
|||
Loading…
Reference in a new issue