Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions host/class/uvc/usb_host_uvc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.4.1] - 2025-11-27

### Added

- Added SOI check to prevent output of corrupted MJPEG frames
- Moved `usb_types_uvc.h` to `private_include` directory
- Fixed bulk transfer EOF handling to improve robustness

## [2.4.0] - 2025-11-21

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,22 @@
#define EXAMPLE_NUMBER_OF_STREAMS (1) // This example shows how to control multiple UVC streams. Set this to 1 if you camera offers only 1 stream
#define EXAMPLE_USE_SDCARD (0) // SD card on P4 evaluation board will be initialized

#define UVC_DESC_DWFRAMEINTERVAL_TO_FPS(dwFrameInterval) (((dwFrameInterval) != 0) ? 10000000 / ((float)(dwFrameInterval)) : 0)

static const char *TAG = "UVC example";
static QueueHandle_t rx_frames_queue[EXAMPLE_NUMBER_OF_STREAMS];
static int device_count = 0;
static bool dev_connected = false;
static uvc_host_frame_info_t *frame_info_list[EXAMPLE_NUMBER_OF_STREAMS] = {NULL};
static size_t frame_info_list_size[EXAMPLE_NUMBER_OF_STREAMS] = {0};

static const char *FORMAT_STR[] = {
"FORMAT_UNDEFINED",
"FORMAT_MJPEG",
"FORMAT_YUY2",
"FORMAT_H264",
"FORMAT_H265",
};

bool frame_callback(const uvc_host_frame_t *frame, void *user_ctx)
{
Expand All @@ -63,6 +76,7 @@ static void stream_callback(const uvc_host_stream_event_data_t *event, void *use
case UVC_HOST_DEVICE_DISCONNECTED:
ESP_LOGI(TAG, "Device suddenly disconnected");
dev_connected = false;
device_count--;
ESP_ERROR_CHECK(uvc_host_stream_close(event->device_disconnected.stream_hdl));
break;
case UVC_HOST_FRAME_BUFFER_OVERFLOW:
Expand Down Expand Up @@ -95,7 +109,7 @@ static void usb_lib_task(void *arg)

static void frame_handling_task(void *arg)
{
const uvc_host_stream_config_t *stream_config = (const uvc_host_stream_config_t *)arg;
const uvc_host_stream_config_t *stream_config = (uvc_host_stream_config_t *)arg;
QueueHandle_t frame_q = *((QueueHandle_t *)(stream_config->user_ctx));
const int uvc_index = stream_config->usb.uvc_stream_index;

Expand Down Expand Up @@ -153,52 +167,8 @@ static void frame_handling_task(void *arg)
}
}

static const uvc_host_stream_config_t stream_mjpeg_config = {
.event_cb = stream_callback,
.frame_cb = frame_callback,
.user_ctx = &rx_frames_queue[0],
.usb = {
.vid = EXAMPLE_USB_DEVICE_VID,
.pid = EXAMPLE_USB_DEVICE_PID,
.uvc_stream_index = 0,
},
.vs_format = {
.h_res = 720,
.v_res = 1280,
.fps = 15,
.format = UVC_VS_FORMAT_MJPEG,
},
.advanced = {
.number_of_frame_buffers = EXAMPLE_FRAME_COUNT,
.frame_size = 0,
.number_of_urbs = 4,
.urb_size = 10 * 1024,
},
};

#if EXAMPLE_NUMBER_OF_STREAMS > 1
static const uvc_host_stream_config_t stream_h265_config = {
.event_cb = stream_callback,
.frame_cb = frame_callback,
.user_ctx = &rx_frames_queue[1],
.usb = {
.vid = EXAMPLE_USB_DEVICE_VID,
.pid = EXAMPLE_USB_DEVICE_PID,
.uvc_stream_index = 1,
},
.vs_format = {
.h_res = 1280,
.v_res = 720,
.fps = 15,
.format = UVC_VS_FORMAT_H265,
},
.advanced = {
.number_of_frame_buffers = EXAMPLE_FRAME_COUNT,
.frame_size = 0,
.number_of_urbs = 4,
.urb_size = 10 * 1024,
},
};

#endif // EXAMPLE_NUMBER_OF_STREAMS > 1

#if EXAMPLE_USE_SDCARD
Expand Down Expand Up @@ -279,6 +249,85 @@ void app_init_sdcard(void)
}
#endif // EXAMPLE_USE_SDCARD

static void start_uvc_frame_handling_tasks(void)
{
uvc_host_stream_config_t _stream_config = {
.event_cb = stream_callback,
.frame_cb = frame_callback,
.user_ctx = &rx_frames_queue[0],
.usb = {
.vid = EXAMPLE_USB_DEVICE_VID,
.pid = EXAMPLE_USB_DEVICE_PID,
.uvc_stream_index = 0,
},
.vs_format = {
.h_res = frame_info_list[0][0].h_res,
.v_res = frame_info_list[0][0].v_res,
.fps = UVC_DESC_DWFRAMEINTERVAL_TO_FPS(frame_info_list[0][0].default_interval),
.format = UVC_VS_FORMAT_MJPEG,
},
.advanced = {
.number_of_frame_buffers = EXAMPLE_FRAME_COUNT,
.frame_size = 0,
.number_of_urbs = 4,
.urb_size = 10 * 1024,
},
};
static uvc_host_stream_config_t stream_mjpeg_config;
memcpy(&stream_mjpeg_config, &_stream_config, sizeof(uvc_host_stream_config_t));
BaseType_t task_created = xTaskCreatePinnedToCore(frame_handling_task, "mjpeg_handling", 4096, (void *)&stream_mjpeg_config, EXAMPLE_USB_HOST_PRIORITY - 2, NULL, tskNO_AFFINITY);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Stack-local config passed to task causes use-after-free

The stream_mjpeg_config and stream_h265_config variables are local to start_uvc_frame_handling_tasks() but their addresses are passed to xTaskCreatePinnedToCore. When this function returns, those stack variables go out of scope. The newly created tasks copy the config in frame_handling_task() at line 110, but there is no synchronization to ensure the task runs before the function returns. This creates a race condition where the task may read garbage from deallocated stack memory.

Additional Locations (1)

Fix in Cursor Fix in Web

assert(task_created == pdTRUE);

#if EXAMPLE_NUMBER_OF_STREAMS > 1
vTaskDelay(pdMS_TO_TICKS(1000));
_stream_config.usb.uvc_stream_index = 1;
_stream_config.vs_format.h_res = frame_info_list[1][0].h_res;
_stream_config.vs_format.v_res = frame_info_list[1][0].v_res;
_stream_config.vs_format.fps = UVC_DESC_DWFRAMEINTERVAL_TO_FPS(frame_info_list[1][0].default_interval);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Example accesses uninitialized frame_info_list for second stream

When EXAMPLE_NUMBER_OF_STREAMS > 1, start_uvc_frame_handling_tasks() accesses frame_info_list[1][0] to read h_res, v_res, and default_interval. However, this function is called immediately after the first UVC_HOST_DRIVER_EVENT_DEVICE_CONNECTED callback for stream index 0. The second callback (for stream index 1) increments device_count to 2 and breaks early due to the "Multiple devices connected" check, so frame_info_list[1] is never populated. This results in a NULL pointer dereference.

Fix in Cursor Fix in Web

_stream_config.vs_format.format = UVC_VS_FORMAT_H265;
_stream_config.user_ctx = &rx_frames_queue[1];
static uvc_host_stream_config_t stream_h265_config;
memcpy(&stream_h265_config, &_stream_config, sizeof(uvc_host_stream_config_t));
task_created = xTaskCreatePinnedToCore(frame_handling_task, "h265_handling", 4096, (void *)&stream_h265_config, EXAMPLE_USB_HOST_PRIORITY - 3, NULL, tskNO_AFFINITY);
assert(task_created == pdTRUE);
#endif // EXAMPLE_NUMBER_OF_STREAMS > 1
}

static void uvc_event_cb(const uvc_host_driver_event_data_t *event, void *user_ctx)
{
switch (event->type) {
case UVC_HOST_DRIVER_EVENT_DEVICE_CONNECTED: {
ESP_LOGI(TAG, "Device connected, addr: %d, stream index: %d", event->device_connected.dev_addr, event->device_connected.uvc_stream_index);
int stream_index = event->device_connected.uvc_stream_index;
if (stream_index >= EXAMPLE_NUMBER_OF_STREAMS) {
ESP_LOGW(TAG, "Stream index %d is out of range. Max supported is %d", stream_index, EXAMPLE_NUMBER_OF_STREAMS - 1);
break;
}
device_count++;
if (device_count > 1) {
ESP_LOGW(TAG, "Multiple devices connected. ignoring additional devices.");
break;
}

frame_info_list_size[stream_index] = event->device_connected.frame_info_num;
frame_info_list[stream_index] = calloc(frame_info_list_size[stream_index], sizeof(uvc_host_frame_info_t));
assert(frame_info_list[stream_index]);
uvc_host_get_frame_list(event->device_connected.dev_addr, stream_index, (uvc_host_frame_info_t (*)[])frame_info_list[stream_index], &frame_info_list_size[stream_index]);
for (int i = 0; i < frame_info_list_size[stream_index]; i++) {
ESP_LOGI(TAG, "Camera format: %s %d*%d@%.1ffps",
FORMAT_STR[frame_info_list[stream_index][i].format],
frame_info_list[stream_index][i].h_res,
frame_info_list[stream_index][i].v_res,
UVC_DESC_DWFRAMEINTERVAL_TO_FPS(frame_info_list[stream_index][i].default_interval));
}
start_uvc_frame_handling_tasks();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Multiple streams trigger duplicate task creation

When EXAMPLE_NUMBER_OF_STREAMS is greater than 1, the uvc_event_cb callback is triggered separately for each stream index that connects. Each invocation calls start_uvc_frame_handling_tasks(), which unconditionally creates tasks for all streams. This results in duplicate task instances being created - for example, with 2 streams, the tasks would be created twice, causing 4 task instances instead of 2. The old code created tasks once at startup in app_main(), but the new event-driven approach lacks a guard to ensure tasks are only created once.

Fix in Cursor Fix in Web

break;
}
default:
break;
}
}

/**
* @brief Main application
*/
Expand Down Expand Up @@ -311,15 +360,7 @@ void app_main(void)
.driver_task_priority = EXAMPLE_USB_HOST_PRIORITY + 1,
.xCoreID = tskNO_AFFINITY,
.create_background_task = true,
.event_cb = uvc_event_cb,
};
ESP_ERROR_CHECK(uvc_host_install(&uvc_driver_config));

task_created = xTaskCreatePinnedToCore(frame_handling_task, "mjpeg_handling", 4096, (void *)&stream_mjpeg_config, EXAMPLE_USB_HOST_PRIORITY - 2, NULL, tskNO_AFFINITY);
assert(task_created == pdTRUE);

#if EXAMPLE_NUMBER_OF_STREAMS > 1
vTaskDelay(pdMS_TO_TICKS(1000));
task_created = xTaskCreatePinnedToCore(frame_handling_task, "h265_handling", 4096, (void *)&stream_h265_config, EXAMPLE_USB_HOST_PRIORITY - 3, NULL, tskNO_AFFINITY);
assert(task_created == pdTRUE);
#endif // EXAMPLE_USE_SDCARD
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# This file was generated using idf.py save-defconfig. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 5.4.0 Project Minimal Configuration
#
CONFIG_IDF_TARGET="esp32p4"
CONFIG_ESP32P4_REV_MIN_0=y
CONFIG_RTC_CLK_SRC_EXT_CRYS=y
CONFIG_RTC_CLK_CAL_CYCLES=1024
CONFIG_FREERTOS_HZ=1000
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_SPEED_200M=y

CONFIG_LOG_COLORS=y
CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE=4096
CONFIG_USB_HOST_HW_BUFFER_BIAS_IN=y
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y

CONFIG_ESP32P4_REV_MIN_0=y
CONFIG_RTC_CLK_SRC_EXT_CRYS=y
CONFIG_RTC_CLK_CAL_CYCLES=1024
CONFIG_SPIRAM_SPEED_200M=y
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y
CONFIG_SPIRAM_RODATA=y
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y
CONFIG_SPIRAM_RODATA=y
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
#include <cstdint>

#include "usb/usb_types_stack.h"
#include "usb/usb_types_uvc.h"
#include "usb_types_uvc.h"

extern "C" {
#include "Mockusb_host.h"
Expand Down
2 changes: 1 addition & 1 deletion host/class/uvc/usb_host_uvc/idf_component.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## IDF Component Manager Manifest File
version: "2.4.0"
version: "2.4.1"
description: USB Host UVC driver
url: https://github.com/espressif/esp-usb/tree/master/host/class/uvc/usb_host_uvc
dependencies:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma once

#include "usb/uvc_host.h"
#include "usb_types_uvc.h"

#ifdef __cplusplus
extern "C" {
Expand Down
1 change: 0 additions & 1 deletion host/class/uvc/usb_host_uvc/include/usb/uvc_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

#include <stdbool.h>
#include "usb/usb_host.h"
#include "usb/usb_types_uvc.h"
#include "esp_err.h"

// Use this macros for opening a UVC stream with any VID or PID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// So we include only files with USB specification definitions
// This interface is also used in host_tests
#include "usb/usb_types_ch9.h"
#include "usb/usb_types_uvc.h"
#include "usb_types_uvc.h"

#define UVC_DESC_FPS_TO_DWFRAMEINTERVAL(fps) (((fps) != 0) ? 10000000.0f / (fps) : 0)
#define UVC_DESC_DWFRAMEINTERVAL_TO_FPS(dwFrameInterval) (((dwFrameInterval) != 0) ? 10000000.0f / ((float)(dwFrameInterval)) : 0)
Expand Down
11 changes: 11 additions & 0 deletions host/class/uvc/usb_host_uvc/private_include/uvc_frame_priv.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ static inline void uvc_frame_reset(uvc_host_frame_t *frame)
*/
void uvc_frame_format_update(uvc_stream_t *uvc_stream, const uvc_host_stream_format_t *vs_format);

/**
* @brief Check if UVC payload header is valid
*
* @param[in] hdr Pointer to UVC payload header
* @param[in] packet_len Length of packet in bytes
* @return
* - true: Header is valid
* - false: Header is invalid
*/
bool uvc_frame_payload_header_validate(const uvc_payload_header_t *hdr, size_t packet_len);

#ifdef __cplusplus
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#include "usb/usb_host.h"
#include "usb/uvc_host.h"
#include "usb_types_uvc.h"

#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
Expand Down
Loading
Loading