Skip to content

Commit 473b5df

Browse files
author
Jure Rebernik
committed
uvc: add uncompressed NV12 streaming support
- Add runtime UVC format selection (mjpeg|uncompressed/nv12) - Stream uncompressed NV12 frames from DepthAI with stride/plane-aware repack - Configure UVC configfs uncompressed descriptor as NV12 (uvc-gadget submodule) - Update app default startup format and README Signed-off-by: Jure Rebernik <jure.rebernik@luxonis.com>
1 parent 67b763f commit 473b5df

5 files changed

Lines changed: 131 additions & 25 deletions

File tree

cpp/uvc/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ Running the example in standalone mode builds and deploys it as an OAK app so th
3333
```
3434

3535
`oakctl` uses the provided `oakapp.toml` to build the C++ project inside the Luxonis base container and deploy it to the device. Configuration tweaks such as changing the camera resolution or registering more topics should be done in `src/uvc_example.cpp`, then re-run `oakctl app run ./cpp/uvc`.
36+
37+
### Video format selection
38+
39+
The example supports two UVC stream formats controlled by `UVC_FORMAT` environment variable:
40+
41+
- `nv12` / `uncompressed` (default): Uses DepthAI `NV12` output and exposes UVC uncompressed NV12 format.
42+
- `mjpeg`: Uses `VideoEncoder` and exposes UVC MJPEG format.

cpp/uvc/oakapp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ identifier = "com.example.streaming.uvc"
88
app_version = "3.0.0"
99

1010
# Command to run when the container starts
11-
entrypoint = ["bash", "-c", "/app/uvc-start.sh start"]
11+
entrypoint = ["bash", "-c", "export UVC_FORMAT=nv12 && /app/uvc-start.sh start"]
1212

1313
# Here is the place where you can install all the dependencies that are needed at run-time
1414
prepare_container = [

cpp/uvc/src/uvc_example.cpp

Lines changed: 94 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
*/
77

88
#include <atomic>
9+
#include <algorithm>
10+
#include <cstdlib>
911
#include <csignal>
12+
#include <cctype>
1013
#include <cstring>
1114
#include <iostream>
15+
#include <string>
16+
#include <vector>
1217

1318
#include "depthai/depthai.hpp"
1419
#include "depthai/pipeline/MessageQueue.hpp"
20+
#include "depthai/pipeline/datatype/Buffer.hpp"
1521
#include "depthai/pipeline/datatype/ImgFrame.hpp"
1622
#include "uvc_example.hpp"
1723

@@ -35,6 +41,28 @@ std::atomic<bool> quitEvent(false);
3541
std::shared_ptr<dai::InputQueue> inputQueue{nullptr};
3642
std::shared_ptr<dai::MessageQueue> outputQueue;
3743

44+
enum class StreamFormat {
45+
MJPEG,
46+
UNCOMPRESSED,
47+
};
48+
49+
static StreamFormat gStreamFormat = StreamFormat::UNCOMPRESSED;
50+
static std::vector<uint8_t> gNv12Buffer;
51+
52+
static StreamFormat parseStreamFormat() {
53+
const char* format = std::getenv("UVC_FORMAT");
54+
if(format == nullptr) return StreamFormat::UNCOMPRESSED;
55+
56+
std::string formatStr(format);
57+
std::transform(formatStr.begin(), formatStr.end(), formatStr.begin(), [](unsigned char c) { return std::tolower(c); });
58+
59+
if(formatStr == "mjpeg") return StreamFormat::MJPEG;
60+
if(formatStr == "uncompressed" || formatStr == "nv12") return StreamFormat::UNCOMPRESSED;
61+
62+
std::cerr << "Unknown UVC_FORMAT=\"" << formatStr << "\", defaulting to uncompressed NV12." << std::endl;
63+
return StreamFormat::UNCOMPRESSED;
64+
}
65+
3866
/* Necessary for and only used by signal handler. */
3967
static struct events *sigint_events;
4068

@@ -48,22 +76,64 @@ void signalHandler(int signum) {
4876

4977
extern "C" void depthai_uvc_get_buffer(struct video_source *s, struct video_buffer *buf) {
5078
unsigned int frame_size, size;
51-
uint8_t *f;
79+
const uint8_t *f;
5280

5381
if(quitEvent) {
5482
std::cout << "depthai_uvc_get_buffer(): Stopping capture due to quit event." << std::endl;
5583
return;
5684
}
5785

58-
auto frame = outputQueue->get<dai::ImgFrame>();
59-
if(frame == nullptr) {
60-
std::cerr << "depthai_uvc_get_buffer(): No frame available." << std::endl;
61-
return;
86+
if(gStreamFormat == StreamFormat::MJPEG) {
87+
auto frame = outputQueue->get<dai::Buffer>();
88+
if(frame == nullptr || frame->getData().empty()) {
89+
std::cerr << "depthai_uvc_get_buffer(): No MJPEG frame available." << std::endl;
90+
return;
91+
}
92+
f = frame->getData().data();
93+
frame_size = frame->getData().size();
94+
} else {
95+
auto frame = outputQueue->get<dai::ImgFrame>();
96+
if(frame == nullptr) {
97+
std::cerr << "depthai_uvc_get_buffer(): No uncompressed frame available." << std::endl;
98+
return;
99+
}
100+
if(frame->getType() != dai::ImgFrame::Type::NV12) {
101+
std::cerr << "depthai_uvc_get_buffer(): Unexpected frame type for uncompressed mode: " << static_cast<int>(frame->getType()) << std::endl;
102+
return;
103+
}
104+
105+
const auto width = frame->getWidth();
106+
const auto height = frame->getHeight();
107+
const auto stride = frame->getStride();
108+
const auto uvPlaneOffset = frame->getPlaneStride(0);
109+
const auto compactNv12FrameSize = (width * height * 3) / 2;
110+
const auto expectedSrcBytes = uvPlaneOffset + (stride * (height / 2));
111+
const auto& data = frame->getData();
112+
113+
if(data.size() < expectedSrcBytes) {
114+
std::cerr << "depthai_uvc_get_buffer(): NV12 frame smaller than expected: have "
115+
<< data.size() << " need " << expectedSrcBytes << std::endl;
116+
return;
117+
}
118+
119+
gNv12Buffer.resize(compactNv12FrameSize);
120+
const auto* src = data.data();
121+
auto* dst = gNv12Buffer.data();
122+
123+
for(uint32_t y = 0; y < height; ++y) {
124+
memcpy(dst + (y * width), src + (y * stride), width);
125+
}
126+
127+
const auto* uvSrc = src + uvPlaneOffset;
128+
auto* uvDst = dst + (width * height);
129+
for(uint32_t y = 0; y < height / 2; ++y) {
130+
memcpy(uvDst + (y * width), uvSrc + (y * stride), width);
131+
}
132+
133+
f = gNv12Buffer.data();
134+
frame_size = static_cast<unsigned int>(gNv12Buffer.size());
62135
}
63136

64-
f = frame->getData().data();
65-
frame_size = frame->getData().size();
66-
67137
size = std::min(frame_size, buf->size);
68138
memcpy(buf->mem, f, size);
69139
buf->bytesused = size;
@@ -90,6 +160,8 @@ int main() {
90160
struct video_source* src;
91161
struct uvc_stream* stream;
92162

163+
gStreamFormat = parseStreamFormat();
164+
93165
depthai_uvc_register_get_buffer(depthai_uvc_get_buffer);
94166

95167
fc = configfs_parse_uvc_function("uvc.0");
@@ -141,13 +213,20 @@ int main() {
141213
// Create nodes
142214
auto camRgb = pipeline.create<dai::node::Camera>()->build(socket);
143215
inputQueue = camRgb->inputControl.createInputQueue();
144-
auto output = camRgb->requestOutput(std::make_pair(1920, 1080), dai::ImgFrame::Type::NV12);
145-
146-
// Create video encoder node
147-
auto encoded = pipeline.create<dai::node::VideoEncoder>();
148-
encoded->setDefaultProfilePreset(30, dai::VideoEncoderProperties::Profile::MJPEG);
149-
output->link(encoded->input);
150-
outputQueue = encoded->bitstream.createOutputQueue(1, false);
216+
constexpr uint32_t width = 1920;
217+
constexpr uint32_t height = 1080;
218+
auto output = camRgb->requestOutput(std::make_pair(width, height), dai::ImgFrame::Type::NV12);
219+
220+
if(gStreamFormat == StreamFormat::MJPEG) {
221+
auto encoded = pipeline.create<dai::node::VideoEncoder>();
222+
encoded->setDefaultProfilePreset(30, dai::VideoEncoderProperties::Profile::MJPEG);
223+
output->link(encoded->input);
224+
outputQueue = encoded->bitstream.createOutputQueue(1, false);
225+
std::cout << "Configured UVC stream format: MJPEG" << std::endl;
226+
} else {
227+
outputQueue = output->createOutputQueue(1, false);
228+
std::cout << "Configured UVC stream format: uncompressed NV12" << std::endl;
229+
}
151230

152231
// Start pipeline
153232
pipeline.start();

cpp/uvc/uvc-gadget

cpp/uvc/uvc-start.sh

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ fi
2525
MANUF="Luxonis"
2626
PRODUCT="Luxonis UVC Camera"
2727
UDC=$(ls /sys/class/udc | head -n1) # will identify the 'first' UDC
28+
: "${UVC_FORMAT:=uncompressed}"
29+
UVC_FORMAT=$(echo "$UVC_FORMAT" | tr '[:upper:]' '[:lower:]')
2830

2931
log "=== Detecting platform:"
3032
log " product : $PRODUCT"
3133
log " udc : $UDC"
3234
log " serial : $SERIAL"
35+
log " format : $UVC_FORMAT"
3336

3437
remove_uvc_gadget() {
3538
if [ ! -d /sys/kernel/config/usb_gadget/g1/functions/uvc.0 ]; then
@@ -101,6 +104,20 @@ create_frame() {
101104
EOF
102105
}
103106

107+
configure_uncompressed_nv12_descriptor() {
108+
FUNCTION=$1
109+
NAME=$2
110+
111+
FRAME_DIR="functions/$FUNCTION/streaming/uncompressed/$NAME/1080p"
112+
FORMAT_DIR="functions/$FUNCTION/streaming/uncompressed/$NAME"
113+
114+
# NV12 is 12bpp (4:2:0), frame size is width * height * 3 / 2.
115+
echo 12 > "$FORMAT_DIR/bBitsPerPixel"
116+
echo $(( 1920 * 1080 * 3 / 2 )) > "$FRAME_DIR/dwMaxVideoFrameBufferSize"
117+
# UVC GUID for NV12: 4e 56 31 32 00 00 10 00 80 00 00 aa 00 38 9b 71
118+
echo -ne '\x4e\x56\x31\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' > "$FORMAT_DIR/guidFormat"
119+
}
120+
104121
create_uvc() {
105122
# Example usage:
106123
# create_uvc <target config> <function name>
@@ -113,17 +130,20 @@ create_uvc() {
113130
pushd "$GADGET/g1" >/dev/null
114131
mkdir "functions/$FUNCTION"
115132

116-
# create_frame "$FUNCTION" 640 360 uncompressed u
117-
# create_frame "$FUNCTION" 1280 720 uncompressed u
118-
# create_frame "$FUNCTION" 320 180 uncompressed u
119-
create_frame "$FUNCTION" 1920 1080 mjpeg m
120-
# create_frame "$FUNCTION" 640 480 mjpeg m
121-
# create_frame "$FUNCTION" 640 360 mjpeg m
133+
if [ "$UVC_FORMAT" = "mjpeg" ]; then
134+
create_frame "$FUNCTION" 1920 1080 mjpeg m
135+
else
136+
create_frame "$FUNCTION" 1920 1080 uncompressed u
137+
configure_uncompressed_nv12_descriptor "$FUNCTION" "u"
138+
fi
122139

123140
mkdir "functions/$FUNCTION/streaming/header/h"
124141
cd "functions/$FUNCTION/streaming/header/h"
125-
# ln -s ../../uncompressed/u
126-
ln -s ../../mjpeg/m
142+
if [ "$UVC_FORMAT" = "mjpeg" ]; then
143+
ln -s ../../mjpeg/m
144+
else
145+
ln -s ../../uncompressed/u
146+
fi
127147
cd ../../class/fs
128148
ln -s ../../header/h
129149
cd ../../class/hs

0 commit comments

Comments
 (0)