blob: e28e55527042565f61610b34fd132602841715d9 [file]
/*
* Copyright (c) 2025 tinyVision.ai Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdlib.h>
#include <sample_usbd.h>
#include <zephyr/device.h>
#include <zephyr/usb/usbd.h>
#include <zephyr/video/video.h>
#include <zephyr/logging/log.h>
#include <zephyr/usb/class/usbd_uvc.h>
LOG_MODULE_REGISTER(uvc_sample, LOG_LEVEL_INF);
const static struct device *const uvc_dev = DEVICE_DT_GET(DT_NODELABEL(uvc));
const static struct device *const video_dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_camera));
static const struct device *const videoenc_dev = DEVICE_DT_GET_OR_NULL(DT_CHOSEN(zephyr_videoenc));
/* Format capabilities of video_dev, used everywhere through the sample */
static struct video_caps video_caps = {.type = VIDEO_BUF_TYPE_OUTPUT};
static struct video_caps videoenc_out_caps = {.type = VIDEO_BUF_TYPE_OUTPUT};
#if DT_HAS_CHOSEN(zephyr_videoenc) && CONFIG_VIDEO_BUFFER_POOL_NUM_MAX < 2
#error CONFIG_VIDEO_BUFFER_POOL_NUM_MAX must be >=2 in order to use a zephyr,videoenc
#endif
static bool app_has_videoenc(void)
{
return (videoenc_dev != NULL);
}
static const struct device *app_uvc_source_dev(void)
{
if (app_has_videoenc()) {
return videoenc_dev;
} else {
return video_dev;
}
}
static struct video_caps *app_uvc_source_caps(void)
{
if (app_has_videoenc()) {
return &videoenc_out_caps;
} else {
return &video_caps;
}
}
/* Pixel formats present in one of the UVC 1.5 standard */
static bool app_is_supported_format(uint32_t pixfmt)
{
return pixfmt == VIDEO_PIX_FMT_JPEG ||
pixfmt == VIDEO_PIX_FMT_YUYV ||
pixfmt == VIDEO_PIX_FMT_NV12 ||
pixfmt == VIDEO_PIX_FMT_H264;
}
static bool app_has_supported_format(void)
{
const struct video_caps *const caps = app_uvc_source_caps();
const struct video_format_cap *const fmts = caps->format_caps;
for (int i = 0; fmts[i].pixelformat != 0; i++) {
if (app_is_supported_format(fmts[i].pixelformat)) {
return true;
}
}
return false;
}
static int app_add_format(uint32_t pixfmt, uint32_t width, uint32_t height, bool has_sup_fmts)
{
const struct device *uvc_src_dev = app_uvc_source_dev();
struct video_format fmt = {
.pixelformat = pixfmt,
.width = width,
.height = height,
.type = VIDEO_BUF_TYPE_OUTPUT,
};
int ret;
/* If the system has any standard pixel format, only propose them to the host */
if (has_sup_fmts && !app_is_supported_format(pixfmt)) {
return 0;
}
/* Set the format to get the size */
ret = video_set_compose_format(uvc_src_dev, &fmt);
if (ret != 0) {
LOG_ERR("Could not set the format of %s to %s %ux%u (size %u)",
video_dev->name, VIDEO_FOURCC_TO_STR(fmt.pixelformat),
fmt.width, fmt.height, fmt.size);
return ret;
}
if (fmt.size > (CONFIG_VIDEO_BUFFER_POOL_HEAP_SIZE / CONFIG_VIDEO_BUFFER_POOL_NUM_MAX)) {
LOG_WRN("Skipping format %ux%u", fmt.width, fmt.height);
return 0;
}
ret = uvc_device_add_format(uvc_dev, &fmt);
if (ret == -ENOMEM) {
/* If there are too many formats, ignore the error, just list fewer formats */
return 0;
}
return ret;
}
struct video_resolution {
uint16_t width;
uint16_t height;
};
static struct video_resolution video_common_fmts[] = {
{ .width = 160, .height = 120, }, /* QQVGA */
{ .width = 320, .height = 240, }, /* QVGA */
{ .width = 640, .height = 480, }, /* VGA */
{ .width = 854, .height = 480, }, /* WVGA */
{ .width = 800, .height = 600, }, /* SVGA */
{ .width = 1280, .height = 720, }, /* HD */
{ .width = 1280, .height = 1024, }, /* SXGA */
{ .width = 1920, .height = 1080, }, /* FHD */
{ .width = 3840, .height = 2160, }, /* UHD */
};
/* Submit to UVC only the formats expected to be working (enough memory for the size, etc.) */
static int app_add_filtered_formats(void)
{
struct video_caps *uvc_src_caps = app_uvc_source_caps();
const bool has_sup_fmts = app_has_supported_format();
int ret;
for (int i = 0; video_caps.format_caps[i].pixelformat != 0; i++) {
/*
* FIXME - in the meantime that auto-negotiation is supported,
* use the resolution list of the camera for NV12 pixelformat
*/
const struct video_format_cap *vcap = &video_caps.format_caps[i];
uint32_t pixelformat;
int count = 1;
if (app_has_videoenc() && vcap->pixelformat != VIDEO_PIX_FMT_NV12) {
continue;
}
if (app_has_videoenc()) {
/*
* FIXME - in the meantime that auto-negotiation is supported,
* when a video encoder is present, always use the first pixelformat.
*/
pixelformat = uvc_src_caps->format_caps[0].pixelformat;
__ASSERT_NO_MSG(pixelformat != 0);
} else {
pixelformat = vcap->pixelformat;
}
ret = app_add_format(pixelformat, vcap->width_min, vcap->height_min,
has_sup_fmts);
if (ret != 0) {
return ret;
}
if (vcap->width_min != vcap->width_max || vcap->height_min != vcap->height_max) {
ret = app_add_format(pixelformat, vcap->width_max, vcap->height_max,
has_sup_fmts);
if (ret != 0) {
return ret;
}
count++;
}
if (vcap->width_step == 0 && vcap->height_step == 0) {
continue;
}
/* RANGE Resolution processing */
for (int j = 0; j < ARRAY_SIZE(video_common_fmts); j++) {
if (count >= CONFIG_APP_VIDEO_MAX_RESOLUTIONS) {
break;
}
if (!IN_RANGE(video_common_fmts[j].width,
vcap->width_min, vcap->width_max) ||
!IN_RANGE(video_common_fmts[j].height,
vcap->height_min, vcap->height_max)) {
continue;
}
if ((video_common_fmts[j].width - vcap->width_min) % vcap->width_step ||
(video_common_fmts[j].height - vcap->height_min) % vcap->height_step) {
continue;
}
ret = app_add_format(pixelformat, video_common_fmts[j].width,
video_common_fmts[j].height, has_sup_fmts);
if (ret != 0) {
return ret;
}
count++;
}
}
return 0;
}
static int app_init_videoenc(const struct device *const dev)
{
int ret;
if (!device_is_ready(dev)) {
LOG_ERR("video encoder %s failed to initialize", dev->name);
return -ENODEV;
}
ret = video_get_caps(dev, &videoenc_out_caps);
if (ret != 0) {
LOG_ERR("Unable to retrieve video encoder output capabilities");
return ret;
}
/*
* FIXME - we should look carefully at both video capture output and encoder input
* caps to detect intermediate format.
* This is where we should define the format which is going to be used
* between the camera and the encoder input
*/
return 0;
}
static int app_configure_videoenc(const struct device *const dev,
uint32_t width, uint32_t height,
uint32_t sink_pixelformat, uint32_t source_pixelformat,
uint32_t nb_buffer)
{
struct video_format fmt = {
.width = width,
.height = height,
};
struct video_buffer *buf;
int ret;
/*
* Need to configure both input & output of the encoder
* and allocate / enqueue buffers to the output of the
* encoder
*/
fmt.type = VIDEO_BUF_TYPE_INPUT;
fmt.pixelformat = sink_pixelformat;
ret = video_set_compose_format(dev, &fmt);
if (ret != 0) {
LOG_ERR("Could not set the %s encoder input format", dev->name);
return ret;
}
fmt.type = VIDEO_BUF_TYPE_OUTPUT;
fmt.pixelformat = source_pixelformat;
ret = video_set_compose_format(dev, &fmt);
if (ret != 0) {
LOG_ERR("Could not set the %s encoder output format", dev->name);
return ret;
}
LOG_INF("Preparing %u buffers of %u bytes for encoder output", nb_buffer, fmt.size);
for (int i = 0; i < nb_buffer; i++) {
buf = video_buffer_aligned_alloc(fmt.size, CONFIG_VIDEO_BUFFER_POOL_ALIGN,
K_NO_WAIT);
if (buf == NULL) {
LOG_ERR("Could not allocate the encoder output buffer");
return -ENOMEM;
}
buf->type = VIDEO_BUF_TYPE_OUTPUT;
ret = video_enqueue(dev, buf);
if (ret != 0) {
LOG_ERR("Could not enqueue video buffer");
return ret;
}
}
return 0;
}
static int app_start_videoenc(const struct device *const dev)
{
int ret;
ret = video_stream_start(dev, VIDEO_BUF_TYPE_OUTPUT);
if (ret != 0) {
LOG_ERR("Failed to start %s output", dev->name);
return ret;
}
ret = video_stream_start(dev, VIDEO_BUF_TYPE_INPUT);
if (ret != 0) {
LOG_ERR("Failed to start %s input", dev->name);
return ret;
}
return 0;
}
int main(void)
{
const struct device *uvc_src_dev = app_uvc_source_dev();
struct usbd_context *sample_usbd;
struct video_buffer *vbuf;
struct video_format fmt = {0};
uint32_t uvc_buf_count = CONFIG_VIDEO_BUFFER_POOL_NUM_MAX;
struct video_frmival frmival = {0};
struct k_poll_signal sig;
struct k_poll_event evt[1];
k_timeout_t timeout = K_FOREVER;
int ret;
if (!device_is_ready(video_dev)) {
LOG_ERR("video source %s failed to initialize", video_dev->name);
return -ENODEV;
}
ret = video_get_caps(video_dev, &video_caps);
if (ret != 0) {
LOG_ERR("Unable to retrieve video capabilities");
return 0;
}
if (app_has_videoenc()) {
ret = app_init_videoenc(videoenc_dev);
if (ret != 0) {
return ret;
}
/* When using encoder, we split the VIDEO_BUFFER_POOL_NUM_MAX in 2 */
uvc_buf_count /= 2;
}
/* Initialize control descriptors from the video device */
uvc_device_init(uvc_dev, uvc_src_dev);
/* Fill the table of formats */
ret = app_add_filtered_formats();
if (ret != 0) {
return ret;
}
/* Prepare the UVC device for being run */
ret = uvc_device_enable(uvc_dev);
if (ret != 0) {
return ret;
}
sample_usbd = sample_usbd_init_device(NULL);
if (sample_usbd == NULL) {
return -ENODEV;
}
ret = usbd_enable(sample_usbd);
if (ret != 0) {
return ret;
}
LOG_INF("Waiting the host to select the video format");
while (true) {
fmt.type = VIDEO_BUF_TYPE_INPUT;
ret = video_get_format(uvc_dev, &fmt);
if (ret == 0) {
break;
}
if (ret != -EAGAIN) {
LOG_ERR("Failed to get the video format");
return ret;
}
k_sleep(K_MSEC(10));
}
ret = video_get_frmival(uvc_dev, &frmival);
if (ret != 0) {
LOG_ERR("Failed to get the video frame interval");
return ret;
}
LOG_INF("The host selected format '%s' %ux%u at frame interval %u/%u",
VIDEO_FOURCC_TO_STR(fmt.pixelformat), fmt.width, fmt.height,
frmival.numerator, frmival.denominator);
if (app_has_videoenc()) {
/*
* FIXME - this is currently hardcoded in NV12 while it should be
* a format that has been validated for both video dev and encoder
*/
ret = app_configure_videoenc(videoenc_dev, fmt.width, fmt.height,
VIDEO_PIX_FMT_NV12, fmt.pixelformat,
CONFIG_VIDEO_BUFFER_POOL_NUM_MAX - uvc_buf_count);
if (ret != 0) {
return ret;
}
}
fmt.type = VIDEO_BUF_TYPE_OUTPUT;
if (app_has_videoenc()) {
/*
* FIXME - this is currently hardcoded in NV12 while it should be
* a format that has been validated for both video dev and encoder
*/
fmt.pixelformat = VIDEO_PIX_FMT_NV12;
}
ret = video_set_compose_format(video_dev, &fmt);
if (ret != 0) {
LOG_ERR("Could not set the format of %s to %s %ux%u (size %u)",
video_dev->name, VIDEO_FOURCC_TO_STR(fmt.pixelformat),
fmt.width, fmt.height, fmt.size);
}
/*
* FIXME - shortcut here since current available encoders do not
* have frmival support for the time being so this is done directly
* at camera level
*/
ret = video_set_frmival(video_dev, &frmival);
if (ret != 0) {
LOG_WRN("Could not set the framerate of %s", video_dev->name);
}
LOG_INF("Preparing %u buffers of %u bytes", uvc_buf_count, fmt.size);
for (int i = 0; i < uvc_buf_count; i++) {
vbuf = video_buffer_aligned_alloc(fmt.size, CONFIG_VIDEO_BUFFER_POOL_ALIGN,
K_NO_WAIT);
if (vbuf == NULL) {
LOG_ERR("Could not allocate the video buffer");
return -ENOMEM;
}
vbuf->type = VIDEO_BUF_TYPE_OUTPUT;
ret = video_enqueue(video_dev, vbuf);
if (ret != 0) {
LOG_ERR("Could not enqueue video buffer");
return ret;
}
}
LOG_DBG("Preparing signaling for %s input/output", uvc_src_dev->name);
k_poll_signal_init(&sig);
k_poll_event_init(&evt[0], K_POLL_TYPE_SIGNAL, K_POLL_MODE_NOTIFY_ONLY, &sig);
ret = video_set_signal(uvc_src_dev, &sig);
if (ret != 0) {
LOG_WRN("Failed to setup the signal on %s output endpoint", uvc_src_dev->name);
timeout = K_MSEC(1);
}
ret = video_set_signal(uvc_dev, &sig);
if (ret != 0) {
LOG_ERR("Failed to setup the signal on %s input endpoint", uvc_dev->name);
return ret;
}
LOG_INF("Starting the video transfer");
if (app_has_videoenc()) {
ret = app_start_videoenc(videoenc_dev);
if (ret != 0) {
return ret;
}
}
ret = video_stream_start(video_dev, VIDEO_BUF_TYPE_OUTPUT);
if (ret != 0) {
LOG_ERR("Failed to start %s", video_dev->name);
return ret;
}
while (true) {
ret = k_poll(evt, ARRAY_SIZE(evt), timeout);
if (ret != 0 && ret != -EAGAIN) {
LOG_ERR("Poll exited with status %d", ret);
return ret;
}
if (app_has_videoenc()) {
ret = video_transfer_buffer(video_dev, uvc_src_dev,
VIDEO_BUF_TYPE_OUTPUT, VIDEO_BUF_TYPE_INPUT,
K_NO_WAIT);
if (ret != 0 && ret != -EAGAIN) {
LOG_ERR("Failed to transfer from %s to %s",
video_dev->name, uvc_src_dev->name);
return ret;
}
}
ret = video_transfer_buffer(uvc_src_dev, uvc_dev,
VIDEO_BUF_TYPE_OUTPUT, VIDEO_BUF_TYPE_INPUT,
K_NO_WAIT);
if (ret != 0 && ret != -EAGAIN) {
LOG_ERR("Failed to transfer from %s to %s",
uvc_src_dev->name, uvc_dev->name);
return ret;
}
if (app_has_videoenc()) {
ret = video_transfer_buffer(uvc_src_dev, video_dev,
VIDEO_BUF_TYPE_INPUT, VIDEO_BUF_TYPE_OUTPUT,
K_NO_WAIT);
if (ret != 0 && ret != -EAGAIN) {
LOG_ERR("Failed to transfer from %s to %s",
uvc_src_dev->name, video_dev->name);
return ret;
}
}
ret = video_transfer_buffer(uvc_dev, uvc_src_dev,
VIDEO_BUF_TYPE_INPUT, VIDEO_BUF_TYPE_OUTPUT,
K_NO_WAIT);
if (ret != 0 && ret != -EAGAIN) {
LOG_ERR("Failed to transfer from %s to %s",
uvc_dev->name, uvc_src_dev->name);
return ret;
}
k_poll_signal_reset(&sig);
}
return 0;
}