feat: node-to-node MJPEG streaming CLIs and shared V4L2 format header
Add stream_send_cli (V4L2 capture → TCP → VIDEO_FRAME) and stream_recv_cli (TCP → threaded frame slot → GLFW display) to exercise end-to-end streaming between two nodes on the same machine or across the network. Add include/stream_stats.h (header-only rolling-window fps/Mbps tracker) and include/v4l2_fmt.h (header-only V4L2 format enumeration shared between v4l2_view_cli and stream_send_cli). Refactor v4l2_view_cli to use the shared header. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ Designed to run on resource-constrained hardware (Raspberry Pi capturing raw MJP
|
||||
- `test_image_cli` — generate test patterns and write PPM output for visual inspection
|
||||
- `xorg_cli` — display a test pattern in the viewer window; exercises scale/anchor modes and text overlays
|
||||
- `v4l2_view_cli` — live camera viewer; auto-selects highest-FPS format, displays FPS overlay
|
||||
- `stream_send_cli` — capture MJPEG from V4L2, connect to a receiver over TCP, stream VIDEO_FRAME messages with per-stream stats
|
||||
- `stream_recv_cli` — listen for incoming TCP stream, display received MJPEG frames with fps/Mbps overlay
|
||||
|
||||
## Structure
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ CLI_SRCS = \
|
||||
query_cli.c \
|
||||
test_image_cli.c \
|
||||
xorg_cli.c \
|
||||
v4l2_view_cli.c
|
||||
v4l2_view_cli.c \
|
||||
stream_send_cli.c \
|
||||
stream_recv_cli.c
|
||||
|
||||
CLI_OBJS = $(CLI_SRCS:%.c=$(CLI_BUILD)/%.o)
|
||||
|
||||
@@ -39,7 +41,9 @@ all: \
|
||||
$(CLI_BUILD)/query_cli \
|
||||
$(CLI_BUILD)/test_image_cli \
|
||||
$(CLI_BUILD)/xorg_cli \
|
||||
$(CLI_BUILD)/v4l2_view_cli
|
||||
$(CLI_BUILD)/v4l2_view_cli \
|
||||
$(CLI_BUILD)/stream_send_cli \
|
||||
$(CLI_BUILD)/stream_recv_cli
|
||||
|
||||
# Module objects delegate to their sub-makes.
|
||||
$(COMMON_OBJ): ; $(MAKE) -C $(ROOT)/src/modules/common
|
||||
@@ -88,6 +92,12 @@ $(CLI_BUILD)/xorg_cli: $(CLI_BUILD)/xorg_cli.o $(TEST_IMAGE_OBJ) $(XORG_OBJ)
|
||||
$(CLI_BUILD)/v4l2_view_cli: $(CLI_BUILD)/v4l2_view_cli.o $(XORG_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(PKG_LDFLAGS)
|
||||
|
||||
$(CLI_BUILD)/stream_send_cli: $(CLI_BUILD)/stream_send_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(PROTOCOL_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread
|
||||
|
||||
$(CLI_BUILD)/stream_recv_cli: $(CLI_BUILD)/stream_recv_cli.o $(COMMON_OBJ) $(SERIAL_OBJ) $(TRANSPORT_OBJ) $(PROTOCOL_OBJ) $(XORG_OBJ)
|
||||
$(CC) $(CFLAGS) -o $@ $^ -lpthread $(PKG_LDFLAGS)
|
||||
|
||||
$(CLI_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
@@ -104,6 +114,8 @@ clean:
|
||||
$(CLI_BUILD)/query_cli \
|
||||
$(CLI_BUILD)/test_image_cli \
|
||||
$(CLI_BUILD)/xorg_cli \
|
||||
$(CLI_BUILD)/v4l2_view_cli
|
||||
$(CLI_BUILD)/v4l2_view_cli \
|
||||
$(CLI_BUILD)/stream_send_cli \
|
||||
$(CLI_BUILD)/stream_recv_cli
|
||||
|
||||
-include $(CLI_OBJS:%.o=%.d)
|
||||
|
||||
336
dev/cli/stream_recv_cli.c
Normal file
336
dev/cli/stream_recv_cli.c
Normal file
@@ -0,0 +1,336 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "xorg.h"
|
||||
#include "transport.h"
|
||||
#include "protocol.h"
|
||||
#include "stream_stats.h"
|
||||
#include "error.h"
|
||||
|
||||
#define DEFAULT_PORT 7700
|
||||
#define DEFAULT_WIN_W 1280
|
||||
#define DEFAULT_WIN_H 720
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Frame slot — single-frame handoff from transport thread to GL thread */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
pthread_mutex_t mutex;
|
||||
pthread_cond_t cond;
|
||||
uint8_t *payload; /* transport-malloc'd; we own */
|
||||
const uint8_t *data; /* points into payload */
|
||||
uint32_t data_len;
|
||||
uint16_t stream_id;
|
||||
int ready; /* 1 = new frame available */
|
||||
int done; /* 1 = sender disconnected */
|
||||
} Frame_Slot;
|
||||
|
||||
static void frame_slot_init(Frame_Slot *s)
|
||||
{
|
||||
pthread_mutex_init(&s->mutex, NULL);
|
||||
pthread_cond_init(&s->cond, NULL);
|
||||
s->payload = NULL;
|
||||
s->data = NULL;
|
||||
s->data_len = 0;
|
||||
s->ready = 0;
|
||||
s->done = 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Receiver state */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
Frame_Slot *slot;
|
||||
int stream_id_filter; /* 0 = accept any */
|
||||
pthread_mutex_t conn_mutex;
|
||||
struct Transport_Conn *conn; /* current sender connection */
|
||||
} Recv_State;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Transport callbacks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void on_connect(struct Transport_Conn *conn, void *userdata)
|
||||
{
|
||||
Recv_State *rs = userdata;
|
||||
fprintf(stderr, "stream_recv_cli: sender connected\n");
|
||||
pthread_mutex_lock(&rs->conn_mutex);
|
||||
rs->conn = conn;
|
||||
pthread_mutex_unlock(&rs->conn_mutex);
|
||||
}
|
||||
|
||||
static void on_disconnect(struct Transport_Conn *conn, void *userdata)
|
||||
{
|
||||
(void)conn;
|
||||
Recv_State *rs = userdata;
|
||||
fprintf(stderr, "stream_recv_cli: sender disconnected\n");
|
||||
|
||||
pthread_mutex_lock(&rs->conn_mutex);
|
||||
rs->conn = NULL;
|
||||
pthread_mutex_unlock(&rs->conn_mutex);
|
||||
|
||||
Frame_Slot *slot = rs->slot;
|
||||
pthread_mutex_lock(&slot->mutex);
|
||||
slot->done = 1;
|
||||
pthread_cond_signal(&slot->cond);
|
||||
pthread_mutex_unlock(&slot->mutex);
|
||||
}
|
||||
|
||||
static void on_frame(struct Transport_Conn *conn,
|
||||
struct Transport_Frame *frame,
|
||||
void *userdata)
|
||||
{
|
||||
Recv_State *rs = userdata;
|
||||
|
||||
if (frame->message_type == PROTO_MSG_VIDEO_FRAME) {
|
||||
struct Proto_Video_Frame vf;
|
||||
struct App_Error err = proto_read_video_frame(
|
||||
frame->payload, frame->payload_length, &vf);
|
||||
if (!APP_IS_OK(err)) {
|
||||
free(frame->payload);
|
||||
return;
|
||||
}
|
||||
if (rs->stream_id_filter && vf.stream_id != (uint16_t)rs->stream_id_filter) {
|
||||
free(frame->payload);
|
||||
return;
|
||||
}
|
||||
|
||||
Frame_Slot *slot = rs->slot;
|
||||
pthread_mutex_lock(&slot->mutex);
|
||||
if (slot->ready) {
|
||||
free(slot->payload); /* drop stale frame — main thread is behind */
|
||||
}
|
||||
slot->payload = frame->payload; /* take ownership */
|
||||
slot->data = vf.data;
|
||||
slot->data_len = vf.data_len;
|
||||
slot->stream_id = vf.stream_id;
|
||||
slot->ready = 1;
|
||||
pthread_cond_signal(&slot->cond);
|
||||
pthread_mutex_unlock(&slot->mutex);
|
||||
/* frame->payload is now owned by slot; do not free here */
|
||||
|
||||
} else if (frame->message_type == PROTO_MSG_CONTROL_REQUEST) {
|
||||
struct Proto_Request_Header hdr;
|
||||
struct App_Error err = proto_read_request_header(
|
||||
frame->payload, frame->payload_length, &hdr);
|
||||
if (APP_IS_OK(err) && hdr.command == PROTO_CMD_STREAM_OPEN) {
|
||||
struct Proto_Stream_Open so;
|
||||
err = proto_read_stream_open(
|
||||
frame->payload, frame->payload_length, &so);
|
||||
if (APP_IS_OK(err)) {
|
||||
fprintf(stderr,
|
||||
"stream_recv_cli: STREAM_OPEN stream_id=%u format=%u origin=%u\n",
|
||||
so.stream_id, so.format, so.origin);
|
||||
proto_write_control_response(conn, hdr.request_id,
|
||||
PROTO_STATUS_OK, NULL, 0);
|
||||
}
|
||||
}
|
||||
free(frame->payload);
|
||||
|
||||
} else {
|
||||
free(frame->payload);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Usage */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void usage(void)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: stream_recv_cli [--port PORT] [--stream-id N]\n"
|
||||
" [--scale stretch|fit|fill|1:1]\n"
|
||||
" [--anchor center|topleft]\n"
|
||||
" [--x N] [--y N]\n"
|
||||
"\n"
|
||||
"Listens for an incoming TCP stream and displays VIDEO_FRAME messages.\n"
|
||||
"Accepts MJPEG streams. Shows per-stream fps and Mbps as an overlay.\n"
|
||||
"\n"
|
||||
"defaults: port=7700 stream-id=0 (any) fit center x=0 y=0\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Main */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
uint16_t port = DEFAULT_PORT;
|
||||
int stream_id_filter = 0;
|
||||
int win_x = 0;
|
||||
int win_y = 0;
|
||||
Xorg_Scale scale = XORG_SCALE_FIT;
|
||||
Xorg_Anchor anchor = XORG_ANCHOR_CENTER;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
||||
port = (uint16_t)atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--stream-id") == 0 && i + 1 < argc) {
|
||||
stream_id_filter = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--x") == 0 && i + 1 < argc) {
|
||||
win_x = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--y") == 0 && i + 1 < argc) {
|
||||
win_y = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--scale") == 0 && i + 1 < argc) {
|
||||
i++;
|
||||
if (strcmp(argv[i], "stretch") == 0) { scale = XORG_SCALE_STRETCH; }
|
||||
else if (strcmp(argv[i], "fit") == 0) { scale = XORG_SCALE_FIT; }
|
||||
else if (strcmp(argv[i], "fill") == 0) { scale = XORG_SCALE_FILL; }
|
||||
else if (strcmp(argv[i], "1:1") == 0) { scale = XORG_SCALE_1_1; }
|
||||
else { fprintf(stderr, "unknown scale: %s\n", argv[i]); usage(); return 1; }
|
||||
} else if (strcmp(argv[i], "--anchor") == 0 && i + 1 < argc) {
|
||||
i++;
|
||||
if (strcmp(argv[i], "center") == 0) { anchor = XORG_ANCHOR_CENTER; }
|
||||
else if (strcmp(argv[i], "topleft") == 0) { anchor = XORG_ANCHOR_TOP_LEFT; }
|
||||
else { fprintf(stderr, "unknown anchor: %s\n", argv[i]); usage(); return 1; }
|
||||
} else {
|
||||
usage(); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!xorg_available()) {
|
||||
fprintf(stderr, "stream_recv_cli: built without HAVE_GLFW — viewer not available\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Frame slot and receiver state */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
Frame_Slot slot;
|
||||
frame_slot_init(&slot);
|
||||
|
||||
Recv_State rs = {0};
|
||||
rs.slot = &slot;
|
||||
rs.stream_id_filter = stream_id_filter;
|
||||
pthread_mutex_init(&rs.conn_mutex, NULL);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Transport server */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
struct Transport_Server_Config cfg = {
|
||||
.port = port,
|
||||
.max_connections = 1,
|
||||
.max_payload = TRANSPORT_DEFAULT_MAX_PAYLOAD,
|
||||
.on_frame = on_frame,
|
||||
.on_connect = on_connect,
|
||||
.on_disconnect = on_disconnect,
|
||||
.userdata = &rs,
|
||||
};
|
||||
|
||||
struct Transport_Server *server = NULL;
|
||||
struct App_Error err = transport_server_create(&server, &cfg);
|
||||
if (!APP_IS_OK(err)) { app_error_print(&err); return 1; }
|
||||
|
||||
err = transport_server_start(server);
|
||||
if (!APP_IS_OK(err)) {
|
||||
app_error_print(&err);
|
||||
transport_server_destroy(server);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "stream_recv_cli: listening on port %u\n", port);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Open viewer */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
Xorg_Viewer *v = xorg_viewer_open(win_x, win_y,
|
||||
DEFAULT_WIN_W, DEFAULT_WIN_H,
|
||||
"stream_recv_cli");
|
||||
if (!v) {
|
||||
fprintf(stderr, "stream_recv_cli: failed to open viewer window\n");
|
||||
transport_server_destroy(server);
|
||||
return 1;
|
||||
}
|
||||
xorg_viewer_set_scale(v, scale);
|
||||
xorg_viewer_set_anchor(v, anchor);
|
||||
xorg_viewer_set_overlay_text(v, 0, 10, 10,
|
||||
"waiting for stream...", 1.0f, 1.0f, 0.8f);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Render loop */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
Stream_Stats stats;
|
||||
stream_stats_init(&stats, (uint16_t)stream_id_filter);
|
||||
|
||||
while (xorg_viewer_handle_events(v)) {
|
||||
/*
|
||||
* Wait for the next frame with a 100ms deadline so we can poll
|
||||
* window events even when no frames arrive.
|
||||
*/
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_nsec += 100000000LL;
|
||||
if (deadline.tv_nsec >= 1000000000LL) {
|
||||
deadline.tv_sec++;
|
||||
deadline.tv_nsec -= 1000000000LL;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&slot.mutex);
|
||||
while (!slot.ready && !slot.done) {
|
||||
int rc = pthread_cond_timedwait(&slot.cond, &slot.mutex, &deadline);
|
||||
if (rc != 0) { break; } /* timeout — poll events */
|
||||
}
|
||||
|
||||
if (!slot.ready) {
|
||||
int done = slot.done;
|
||||
pthread_mutex_unlock(&slot.mutex);
|
||||
if (done) { break; }
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Take ownership of the frame. */
|
||||
uint8_t *payload = slot.payload;
|
||||
const uint8_t *data = slot.data;
|
||||
uint32_t data_len = slot.data_len;
|
||||
uint16_t stream_id = slot.stream_id;
|
||||
slot.payload = NULL;
|
||||
slot.data = NULL;
|
||||
slot.ready = 0;
|
||||
pthread_mutex_unlock(&slot.mutex);
|
||||
|
||||
xorg_viewer_push_mjpeg(v, data, data_len);
|
||||
stream_stats_record_frame(&stats, data_len);
|
||||
free(payload);
|
||||
|
||||
if (stream_stats_tick(&stats)) {
|
||||
char info[64];
|
||||
snprintf(info, sizeof(info), "stream %u %.1f fps %.2f Mbps",
|
||||
stream_id, stats.fps, stats.mbps);
|
||||
xorg_viewer_set_overlay_text(v, 0, 10, 10, info, 1.0f, 1.0f, 0.8f);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Cleanup */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
/* Close the active connection if still open. */
|
||||
pthread_mutex_lock(&rs.conn_mutex);
|
||||
if (rs.conn) {
|
||||
transport_conn_close(rs.conn);
|
||||
rs.conn = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&rs.conn_mutex);
|
||||
|
||||
xorg_viewer_close(v);
|
||||
transport_server_destroy(server);
|
||||
|
||||
pthread_mutex_lock(&slot.mutex);
|
||||
free(slot.payload);
|
||||
pthread_mutex_unlock(&slot.mutex);
|
||||
pthread_mutex_destroy(&slot.mutex);
|
||||
pthread_cond_destroy(&slot.cond);
|
||||
pthread_mutex_destroy(&rs.conn_mutex);
|
||||
|
||||
return 0;
|
||||
}
|
||||
284
dev/cli/stream_send_cli.c
Normal file
284
dev/cli/stream_send_cli.c
Normal file
@@ -0,0 +1,284 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/select.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "v4l2_fmt.h"
|
||||
#include "transport.h"
|
||||
#include "protocol.h"
|
||||
#include "stream_stats.h"
|
||||
#include "error.h"
|
||||
|
||||
#define N_BUFS 4
|
||||
#define DEFAULT_PORT 7700
|
||||
#define DEFAULT_STREAM_ID 1
|
||||
|
||||
typedef struct { void *start; size_t length; } Mmap_Buf;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Transport callbacks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void on_frame(struct Transport_Conn *conn,
|
||||
struct Transport_Frame *frame,
|
||||
void *userdata)
|
||||
{
|
||||
(void)conn; (void)userdata;
|
||||
/* Receiver may send responses; just discard them. */
|
||||
free(frame->payload);
|
||||
}
|
||||
|
||||
static void on_disconnect(struct Transport_Conn *conn, void *userdata)
|
||||
{
|
||||
(void)conn; (void)userdata;
|
||||
fprintf(stderr, "stream_send_cli: disconnected from receiver\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Usage */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void usage(void)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: stream_send_cli [--device PATH] [--host HOST] [--port PORT]\n"
|
||||
" [--stream-id N]\n"
|
||||
"\n"
|
||||
"Captures MJPEG from a V4L2 device and streams VIDEO_FRAME messages over TCP.\n"
|
||||
"Prints frame rate and throughput to stderr every 0.5 s.\n"
|
||||
"\n"
|
||||
"defaults: /dev/video0 127.0.0.1 7700 stream-id=1\n");
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Main */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *device = "/dev/video0";
|
||||
const char *host = "127.0.0.1";
|
||||
uint16_t port = DEFAULT_PORT;
|
||||
uint16_t stream_id = DEFAULT_STREAM_ID;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--device") == 0 && i + 1 < argc) {
|
||||
device = argv[++i];
|
||||
} else if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) {
|
||||
host = argv[++i];
|
||||
} else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) {
|
||||
port = (uint16_t)atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--stream-id") == 0 && i + 1 < argc) {
|
||||
stream_id = (uint16_t)atoi(argv[++i]);
|
||||
} else {
|
||||
usage(); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Open V4L2 device */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
int fd = open(device, O_RDWR | O_NONBLOCK);
|
||||
if (fd < 0) { perror(device); return 1; }
|
||||
|
||||
struct v4l2_capability cap = {0};
|
||||
if (v4l2_xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
|
||||
perror("VIDIOC_QUERYCAP"); close(fd); return 1;
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
|
||||
fprintf(stderr, "%s: not a capture device\n", device); close(fd); return 1;
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
|
||||
fprintf(stderr, "%s: does not support streaming\n", device); close(fd); return 1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Format selection — MJPEG, best FPS then largest size */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
V4l2_Fmt_Option opts[V4L2_FMT_MAX_OPTS];
|
||||
int n = v4l2_enumerate_formats(fd, opts, V4L2_FMT_MAX_OPTS, V4L2_PIX_FMT_MJPEG);
|
||||
if (n == 0) {
|
||||
fprintf(stderr, "%s: no MJPEG formats found\n", device);
|
||||
close(fd); return 1;
|
||||
}
|
||||
const V4l2_Fmt_Option *best = v4l2_select_best(opts, n);
|
||||
|
||||
struct v4l2_format fmt = {0};
|
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
fmt.fmt.pix.pixelformat = best->pixfmt;
|
||||
fmt.fmt.pix.width = (uint32_t)best->w;
|
||||
fmt.fmt.pix.height = (uint32_t)best->h;
|
||||
fmt.fmt.pix.field = V4L2_FIELD_ANY;
|
||||
if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
|
||||
perror("VIDIOC_S_FMT"); close(fd); return 1;
|
||||
}
|
||||
|
||||
int width = (int)fmt.fmt.pix.width;
|
||||
int height = (int)fmt.fmt.pix.height;
|
||||
int fps_n = best->fps_n;
|
||||
int fps_d = best->fps_d;
|
||||
|
||||
{
|
||||
struct v4l2_streamparm parm = {0};
|
||||
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
parm.parm.capture.timeperframe.numerator = (uint32_t)fps_d;
|
||||
parm.parm.capture.timeperframe.denominator = (uint32_t)fps_n;
|
||||
v4l2_xioctl(fd, VIDIOC_S_PARM, &parm);
|
||||
if (v4l2_xioctl(fd, VIDIOC_G_PARM, &parm) == 0 &&
|
||||
parm.parm.capture.timeperframe.denominator > 0) {
|
||||
fps_n = (int)parm.parm.capture.timeperframe.denominator;
|
||||
fps_d = (int)parm.parm.capture.timeperframe.numerator;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "device: %s (%s)\n", device, (char *)cap.card);
|
||||
fprintf(stderr, "format: MJPEG %dx%d target=%.1f fps\n",
|
||||
width, height,
|
||||
fps_d > 0 ? (double)fps_n / fps_d : 0.0);
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Mmap buffers + stream on */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
struct v4l2_requestbuffers req = {0};
|
||||
req.count = N_BUFS;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
if (v4l2_xioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
|
||||
perror("VIDIOC_REQBUFS"); close(fd); return 1;
|
||||
}
|
||||
|
||||
Mmap_Buf bufs[N_BUFS] = {0};
|
||||
for (unsigned i = 0; i < req.count; i++) {
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
if (v4l2_xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
|
||||
perror("VIDIOC_QUERYBUF"); close(fd); return 1;
|
||||
}
|
||||
bufs[i].length = buf.length;
|
||||
bufs[i].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED, fd, buf.m.offset);
|
||||
if (bufs[i].start == MAP_FAILED) {
|
||||
perror("mmap"); close(fd); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (unsigned i = 0; i < req.count; i++) {
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
if (v4l2_xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
perror("VIDIOC_QBUF"); close(fd); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
enum v4l2_buf_type stream_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (v4l2_xioctl(fd, VIDIOC_STREAMON, &stream_type) < 0) {
|
||||
perror("VIDIOC_STREAMON"); close(fd); return 1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Connect to receiver */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
struct Transport_Conn *conn = NULL;
|
||||
struct App_Error err = transport_connect(&conn, host, port,
|
||||
TRANSPORT_DEFAULT_MAX_PAYLOAD,
|
||||
on_frame, on_disconnect, NULL);
|
||||
if (!APP_IS_OK(err)) {
|
||||
app_error_print(&err);
|
||||
v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
close(fd); return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "connected to %s:%u stream_id=%u\n", host, port, stream_id);
|
||||
|
||||
err = proto_write_stream_open(conn, 1 /* request_id */, stream_id,
|
||||
PROTO_FORMAT_MJPEG,
|
||||
0 /* pixel_format: compressed */,
|
||||
PROTO_ORIGIN_DEVICE_NATIVE);
|
||||
if (!APP_IS_OK(err)) {
|
||||
app_error_print(&err);
|
||||
transport_conn_close(conn);
|
||||
v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
close(fd); return 1;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Capture + send loop */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
Stream_Stats stats;
|
||||
stream_stats_init(&stats, stream_id);
|
||||
|
||||
while (1) {
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
struct timeval tv = {1, 0};
|
||||
int r = select(fd + 1, &fds, NULL, NULL, &tv);
|
||||
|
||||
if (r < 0) {
|
||||
if (errno == EINTR) { continue; }
|
||||
perror("select"); break;
|
||||
}
|
||||
if (r == 0) {
|
||||
fprintf(stderr, "stream_send_cli: select timeout — no frames\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
if (v4l2_xioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
|
||||
if (errno == EAGAIN) { continue; }
|
||||
perror("VIDIOC_DQBUF"); break;
|
||||
}
|
||||
|
||||
const uint8_t *data = (const uint8_t *)bufs[buf.index].start;
|
||||
uint32_t nbytes = buf.bytesused;
|
||||
|
||||
err = proto_write_video_frame(conn, stream_id, data, nbytes);
|
||||
|
||||
if (v4l2_xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
perror("VIDIOC_QBUF");
|
||||
if (!APP_IS_OK(err)) { app_error_print(&err); }
|
||||
break;
|
||||
}
|
||||
|
||||
if (!APP_IS_OK(err)) {
|
||||
app_error_print(&err);
|
||||
break;
|
||||
}
|
||||
|
||||
stream_stats_record_frame(&stats, nbytes);
|
||||
if (stream_stats_tick(&stats)) {
|
||||
fprintf(stderr, "stream %u %.1f fps %.2f Mbps\n",
|
||||
stream_id, stats.fps, stats.mbps);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- */
|
||||
/* Cleanup */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
transport_conn_close(conn);
|
||||
v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
for (unsigned i = 0; i < req.count; i++) {
|
||||
if (bufs[i].start && bufs[i].start != MAP_FAILED) {
|
||||
munmap(bufs[i].start, bufs[i].length);
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
@@ -3,157 +3,14 @@
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/select.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#include "v4l2_fmt.h"
|
||||
#include "xorg.h"
|
||||
|
||||
#define N_BUFS 4
|
||||
#define MAX_OPTS 1024
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Format option — one (pixfmt, size, fps) combination */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
uint32_t pixfmt;
|
||||
int w, h;
|
||||
int fps_n; /* fps = fps_n / fps_d */
|
||||
int fps_d;
|
||||
} Fmt_Option;
|
||||
|
||||
/* fps_a > fps_b ? */
|
||||
static int fps_gt(const Fmt_Option *a, const Fmt_Option *b)
|
||||
{
|
||||
return (long long)a->fps_n * b->fps_d > (long long)b->fps_n * a->fps_d;
|
||||
}
|
||||
|
||||
static int fps_eq(const Fmt_Option *a, const Fmt_Option *b)
|
||||
{
|
||||
return (long long)a->fps_n * b->fps_d == (long long)b->fps_n * a->fps_d;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Format enumeration */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
typedef struct {
|
||||
Fmt_Option *opts;
|
||||
int n;
|
||||
int max;
|
||||
} Opt_List;
|
||||
|
||||
static void opt_push(Opt_List *l, uint32_t pixfmt, int w, int h, int fps_n, int fps_d)
|
||||
{
|
||||
if (l->n >= l->max) { return; }
|
||||
l->opts[l->n++] = (Fmt_Option){ pixfmt, w, h, fps_n, fps_d };
|
||||
}
|
||||
|
||||
static int xioctl(int fd, unsigned long req, void *arg)
|
||||
{
|
||||
int r;
|
||||
do { r = ioctl(fd, req, arg); } while (r == -1 && errno == EINTR);
|
||||
return r;
|
||||
}
|
||||
|
||||
static void collect_intervals(int fd, uint32_t pixfmt, int w, int h, Opt_List *l)
|
||||
{
|
||||
struct v4l2_frmivalenum fie = {0};
|
||||
fie.pixel_format = pixfmt;
|
||||
fie.width = (uint32_t)w;
|
||||
fie.height = (uint32_t)h;
|
||||
|
||||
for (fie.index = 0; xioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fie) == 0; fie.index++) {
|
||||
if (fie.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
|
||||
opt_push(l, pixfmt, w, h,
|
||||
(int)fie.discrete.denominator,
|
||||
(int)fie.discrete.numerator);
|
||||
} else {
|
||||
/* Stepwise/continuous: record the fastest (minimum) interval. */
|
||||
opt_push(l, pixfmt, w, h,
|
||||
(int)fie.stepwise.min.denominator,
|
||||
(int)fie.stepwise.min.numerator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void collect_sizes(int fd, uint32_t pixfmt, Opt_List *l)
|
||||
{
|
||||
struct v4l2_frmsizeenum fse = {0};
|
||||
fse.pixel_format = pixfmt;
|
||||
|
||||
for (fse.index = 0; xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fse) == 0; fse.index++) {
|
||||
if (fse.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
|
||||
collect_intervals(fd, pixfmt,
|
||||
(int)fse.discrete.width,
|
||||
(int)fse.discrete.height, l);
|
||||
} else {
|
||||
/* Stepwise/continuous: only probe the maximum size. */
|
||||
collect_intervals(fd, pixfmt,
|
||||
(int)fse.stepwise.max_width,
|
||||
(int)fse.stepwise.max_height, l);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Enumerate all (pixfmt, size, fps) combinations the device supports.
|
||||
* Filtered to formats we can handle (MJPEG, YUYV).
|
||||
* If fmt_filter is non-zero, only that pixel format is considered.
|
||||
*/
|
||||
static int enumerate_formats(int fd, Fmt_Option *buf, int buf_max,
|
||||
uint32_t fmt_filter)
|
||||
{
|
||||
Opt_List l = { buf, 0, buf_max };
|
||||
|
||||
struct v4l2_fmtdesc fd_desc = {0};
|
||||
fd_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
|
||||
for (fd_desc.index = 0;
|
||||
xioctl(fd, VIDIOC_ENUM_FMT, &fd_desc) == 0;
|
||||
fd_desc.index++) {
|
||||
uint32_t pf = fd_desc.pixelformat;
|
||||
if (pf != V4L2_PIX_FMT_MJPEG && pf != V4L2_PIX_FMT_YUYV) { continue; }
|
||||
if (fmt_filter && pf != fmt_filter) { continue; }
|
||||
collect_sizes(fd, pf, &l);
|
||||
}
|
||||
return l.n;
|
||||
}
|
||||
|
||||
/*
|
||||
* Select the best option from the list:
|
||||
* 1. Highest FPS
|
||||
* 2. Largest area (w×h) among equal-FPS entries
|
||||
* 3. MJPEG preferred over YUYV for equal FPS and area
|
||||
*/
|
||||
static const Fmt_Option *select_best(const Fmt_Option *opts, int n)
|
||||
{
|
||||
if (n == 0) { return NULL; }
|
||||
const Fmt_Option *best = &opts[0];
|
||||
for (int i = 1; i < n; i++) {
|
||||
const Fmt_Option *o = &opts[i];
|
||||
if (fps_gt(o, best)) {
|
||||
best = o;
|
||||
} else if (fps_eq(o, best)) {
|
||||
int o_area = o->w * o->h;
|
||||
int b_area = best->w * best->h;
|
||||
if (o_area > b_area) {
|
||||
best = o;
|
||||
} else if (o_area == b_area &&
|
||||
o->pixfmt == V4L2_PIX_FMT_MJPEG &&
|
||||
best->pixfmt != V4L2_PIX_FMT_MJPEG) {
|
||||
best = o;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* YUYV → planar YUV420 conversion */
|
||||
@@ -269,7 +126,7 @@ int main(int argc, char **argv)
|
||||
if (fd < 0) { perror(device); return 1; }
|
||||
|
||||
struct v4l2_capability cap = {0};
|
||||
if (xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) {
|
||||
perror("VIDIOC_QUERYCAP"); close(fd); return 1;
|
||||
}
|
||||
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
|
||||
@@ -301,14 +158,14 @@ int main(int argc, char **argv)
|
||||
use_mjpeg = 0;
|
||||
if (fmt_filter != V4L2_PIX_FMT_YUYV) {
|
||||
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
|
||||
if (xioctl(fd, VIDIOC_S_FMT, &fmt) == 0 &&
|
||||
if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) == 0 &&
|
||||
fmt.fmt.pix.pixelformat == V4L2_PIX_FMT_MJPEG) {
|
||||
use_mjpeg = 1;
|
||||
}
|
||||
}
|
||||
if (!use_mjpeg) {
|
||||
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
|
||||
if (xioctl(fd, VIDIOC_S_FMT, &fmt) < 0 ||
|
||||
if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) < 0 ||
|
||||
fmt.fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV) {
|
||||
fprintf(stderr, "%s: could not set %dx%d in MJPEG or YUYV\n",
|
||||
device, req_width, req_height);
|
||||
@@ -321,16 +178,16 @@ int main(int argc, char **argv)
|
||||
sel_fps_n = 0; sel_fps_d = 1; /* unknown until G_PARM below */
|
||||
} else {
|
||||
/* Enumerate all supported modes and pick the best. */
|
||||
Fmt_Option *opts = malloc(MAX_OPTS * sizeof(*opts));
|
||||
V4l2_Fmt_Option *opts = malloc(V4L2_FMT_MAX_OPTS * sizeof(*opts));
|
||||
if (!opts) { fprintf(stderr, "out of memory\n"); close(fd); return 1; }
|
||||
|
||||
int n = enumerate_formats(fd, opts, MAX_OPTS, fmt_filter);
|
||||
int n = v4l2_enumerate_formats(fd, opts, V4L2_FMT_MAX_OPTS, fmt_filter);
|
||||
if (n == 0) {
|
||||
fprintf(stderr, "%s: no usable formats found (MJPEG/YUYV)\n", device);
|
||||
free(opts); close(fd); return 1;
|
||||
}
|
||||
|
||||
const Fmt_Option *best = select_best(opts, n);
|
||||
const V4l2_Fmt_Option *best = v4l2_select_best(opts, n);
|
||||
|
||||
/* Apply the selected format. */
|
||||
struct v4l2_format fmt = {0};
|
||||
@@ -339,7 +196,7 @@ int main(int argc, char **argv)
|
||||
fmt.fmt.pix.width = (uint32_t)best->w;
|
||||
fmt.fmt.pix.height = (uint32_t)best->h;
|
||||
fmt.fmt.pix.field = V4L2_FIELD_ANY;
|
||||
if (xioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
|
||||
perror("VIDIOC_S_FMT"); free(opts); close(fd); return 1;
|
||||
}
|
||||
|
||||
@@ -358,9 +215,9 @@ int main(int argc, char **argv)
|
||||
parm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
parm.parm.capture.timeperframe.numerator = (uint32_t)sel_fps_d;
|
||||
parm.parm.capture.timeperframe.denominator = (uint32_t)sel_fps_n;
|
||||
xioctl(fd, VIDIOC_S_PARM, &parm);
|
||||
v4l2_xioctl(fd, VIDIOC_S_PARM, &parm);
|
||||
/* Read back what the driver actually set. */
|
||||
if (xioctl(fd, VIDIOC_G_PARM, &parm) == 0 &&
|
||||
if (v4l2_xioctl(fd, VIDIOC_G_PARM, &parm) == 0 &&
|
||||
parm.parm.capture.timeperframe.denominator > 0) {
|
||||
sel_fps_n = (int)parm.parm.capture.timeperframe.denominator;
|
||||
sel_fps_d = (int)parm.parm.capture.timeperframe.numerator;
|
||||
@@ -380,7 +237,7 @@ int main(int argc, char **argv)
|
||||
req.count = N_BUFS;
|
||||
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
req.memory = V4L2_MEMORY_MMAP;
|
||||
if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
|
||||
perror("VIDIOC_REQBUFS"); close(fd); return 1;
|
||||
}
|
||||
|
||||
@@ -390,7 +247,7 @@ int main(int argc, char **argv)
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
if (xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
|
||||
perror("VIDIOC_QUERYBUF"); close(fd); return 1;
|
||||
}
|
||||
bufs[i].length = buf.length;
|
||||
@@ -406,13 +263,13 @@ int main(int argc, char **argv)
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
buf.index = i;
|
||||
if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
perror("VIDIOC_QBUF"); close(fd); return 1;
|
||||
}
|
||||
}
|
||||
|
||||
enum v4l2_buf_type stream_type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
if (xioctl(fd, VIDIOC_STREAMON, &stream_type) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_STREAMON, &stream_type) < 0) {
|
||||
perror("VIDIOC_STREAMON"); close(fd); return 1;
|
||||
}
|
||||
|
||||
@@ -423,7 +280,7 @@ int main(int argc, char **argv)
|
||||
Xorg_Viewer *v = xorg_viewer_open(win_x, win_y, width, height, "v4l2_view_cli");
|
||||
if (!v) {
|
||||
fprintf(stderr, "v4l2_view_cli: failed to open viewer window\n");
|
||||
xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
close(fd); return 1;
|
||||
}
|
||||
xorg_viewer_set_scale(v, scale);
|
||||
@@ -439,7 +296,7 @@ int main(int argc, char **argv)
|
||||
if (!yuv420_buf) {
|
||||
fprintf(stderr, "v4l2_view_cli: out of memory\n");
|
||||
xorg_viewer_close(v);
|
||||
xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
close(fd); return 1;
|
||||
}
|
||||
}
|
||||
@@ -480,7 +337,7 @@ int main(int argc, char **argv)
|
||||
struct v4l2_buffer buf = {0};
|
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
buf.memory = V4L2_MEMORY_MMAP;
|
||||
if (xioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
|
||||
if (errno == EAGAIN) { continue; }
|
||||
perror("VIDIOC_DQBUF"); break;
|
||||
}
|
||||
@@ -497,7 +354,7 @@ int main(int argc, char **argv)
|
||||
xorg_viewer_push_yuv420(v, y_p, cb_p, cr_p, width, height);
|
||||
}
|
||||
|
||||
if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
if (v4l2_xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
|
||||
perror("VIDIOC_QBUF"); break;
|
||||
}
|
||||
|
||||
@@ -525,7 +382,7 @@ int main(int argc, char **argv)
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
xorg_viewer_close(v);
|
||||
xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
v4l2_xioctl(fd, VIDIOC_STREAMOFF, &stream_type);
|
||||
for (unsigned i = 0; i < req.count; i++) {
|
||||
if (bufs[i].start && bufs[i].start != MAP_FAILED) {
|
||||
munmap(bufs[i].start, bufs[i].length);
|
||||
|
||||
76
include/stream_stats.h
Normal file
76
include/stream_stats.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Lightweight per-stream statistics tracker.
|
||||
* Header-only; include wherever stream send/receive happens.
|
||||
*
|
||||
* Usage:
|
||||
* Stream_Stats s;
|
||||
* stream_stats_init(&s, stream_id);
|
||||
*
|
||||
* // on each frame:
|
||||
* stream_stats_record_frame(&s, byte_count);
|
||||
*
|
||||
* // periodically (e.g. after every frame):
|
||||
* if (stream_stats_tick(&s)) {
|
||||
* printf("%.1f fps %.2f Mbps\n", s.fps, s.mbps);
|
||||
* }
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#define STREAM_STATS_INTERVAL 0.5 /* recompute rates every 0.5 s */
|
||||
|
||||
typedef struct {
|
||||
uint16_t stream_id;
|
||||
|
||||
/* Lifetime counters — never reset. */
|
||||
uint64_t total_frames;
|
||||
uint64_t total_bytes;
|
||||
|
||||
/* Rolling window — reset each time rates are computed. */
|
||||
uint64_t window_frames;
|
||||
uint64_t window_bytes;
|
||||
struct timespec window_start;
|
||||
|
||||
/* Last computed rates. */
|
||||
float fps;
|
||||
float mbps;
|
||||
} Stream_Stats;
|
||||
|
||||
static inline void stream_stats_init(Stream_Stats *s, uint16_t stream_id)
|
||||
{
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->stream_id = stream_id;
|
||||
clock_gettime(CLOCK_MONOTONIC, &s->window_start);
|
||||
}
|
||||
|
||||
/* Call once per received/sent frame. */
|
||||
static inline void stream_stats_record_frame(Stream_Stats *s, uint32_t nbytes)
|
||||
{
|
||||
s->total_frames++;
|
||||
s->total_bytes += nbytes;
|
||||
s->window_frames++;
|
||||
s->window_bytes += nbytes;
|
||||
}
|
||||
|
||||
/*
|
||||
* Recompute fps and mbps if enough time has elapsed.
|
||||
* Returns 1 when rates were updated, 0 otherwise.
|
||||
*/
|
||||
static inline int stream_stats_tick(Stream_Stats *s)
|
||||
{
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
double elapsed = (double)(now.tv_sec - s->window_start.tv_sec) +
|
||||
(double)(now.tv_nsec - s->window_start.tv_nsec) * 1e-9;
|
||||
if (elapsed < STREAM_STATS_INTERVAL) { return 0; }
|
||||
s->fps = (float)((double)s->window_frames / elapsed);
|
||||
s->mbps = (float)((double)s->window_bytes * 8.0 / elapsed / 1e6);
|
||||
s->window_frames = 0;
|
||||
s->window_bytes = 0;
|
||||
s->window_start = now;
|
||||
return 1;
|
||||
}
|
||||
155
include/v4l2_fmt.h
Normal file
155
include/v4l2_fmt.h
Normal file
@@ -0,0 +1,155 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Header-only V4L2 format enumeration.
|
||||
* Enumerates (pixfmt, size, fps) combinations for MJPEG/YUYV capture devices.
|
||||
*
|
||||
* Usage:
|
||||
* V4l2_Fmt_Option opts[V4L2_FMT_MAX_OPTS];
|
||||
* int n = v4l2_enumerate_formats(fd, opts, V4L2_FMT_MAX_OPTS, 0);
|
||||
* const V4l2_Fmt_Option *best = v4l2_select_best(opts, n);
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#define V4L2_FMT_MAX_OPTS 1024
|
||||
|
||||
typedef struct {
|
||||
uint32_t pixfmt;
|
||||
int w, h;
|
||||
int fps_n; /* fps = fps_n / fps_d */
|
||||
int fps_d;
|
||||
} V4l2_Fmt_Option;
|
||||
|
||||
static inline int v4l2_xioctl(int fd, unsigned long req, void *arg)
|
||||
{
|
||||
int r;
|
||||
do { r = ioctl(fd, req, arg); } while (r == -1 && errno == EINTR);
|
||||
return r;
|
||||
}
|
||||
|
||||
static inline int v4l2_fmt_fps_gt(const V4l2_Fmt_Option *a, const V4l2_Fmt_Option *b)
|
||||
{
|
||||
return (long long)a->fps_n * b->fps_d > (long long)b->fps_n * a->fps_d;
|
||||
}
|
||||
|
||||
static inline int v4l2_fmt_fps_eq(const V4l2_Fmt_Option *a, const V4l2_Fmt_Option *b)
|
||||
{
|
||||
return (long long)a->fps_n * b->fps_d == (long long)b->fps_n * a->fps_d;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
V4l2_Fmt_Option *opts;
|
||||
int n;
|
||||
int max;
|
||||
} V4l2_Opt_List;
|
||||
|
||||
static inline void v4l2_opt_push(V4l2_Opt_List *l, uint32_t pixfmt,
|
||||
int w, int h, int fps_n, int fps_d)
|
||||
{
|
||||
if (l->n >= l->max) { return; }
|
||||
l->opts[l->n++] = (V4l2_Fmt_Option){ pixfmt, w, h, fps_n, fps_d };
|
||||
}
|
||||
|
||||
static inline void v4l2_collect_intervals(int fd, uint32_t pixfmt, int w, int h,
|
||||
V4l2_Opt_List *l)
|
||||
{
|
||||
struct v4l2_frmivalenum fie = {0};
|
||||
fie.pixel_format = pixfmt;
|
||||
fie.width = (uint32_t)w;
|
||||
fie.height = (uint32_t)h;
|
||||
|
||||
for (fie.index = 0;
|
||||
v4l2_xioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fie) == 0;
|
||||
fie.index++) {
|
||||
if (fie.type == V4L2_FRMIVAL_TYPE_DISCRETE) {
|
||||
v4l2_opt_push(l, pixfmt, w, h,
|
||||
(int)fie.discrete.denominator,
|
||||
(int)fie.discrete.numerator);
|
||||
} else {
|
||||
/* Stepwise/continuous: record the fastest (minimum) interval. */
|
||||
v4l2_opt_push(l, pixfmt, w, h,
|
||||
(int)fie.stepwise.min.denominator,
|
||||
(int)fie.stepwise.min.numerator);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline void v4l2_collect_sizes(int fd, uint32_t pixfmt, V4l2_Opt_List *l)
|
||||
{
|
||||
struct v4l2_frmsizeenum fse = {0};
|
||||
fse.pixel_format = pixfmt;
|
||||
|
||||
for (fse.index = 0;
|
||||
v4l2_xioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fse) == 0;
|
||||
fse.index++) {
|
||||
if (fse.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
|
||||
v4l2_collect_intervals(fd, pixfmt,
|
||||
(int)fse.discrete.width,
|
||||
(int)fse.discrete.height, l);
|
||||
} else {
|
||||
/* Stepwise/continuous: only probe the maximum size. */
|
||||
v4l2_collect_intervals(fd, pixfmt,
|
||||
(int)fse.stepwise.max_width,
|
||||
(int)fse.stepwise.max_height, l);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Enumerate all (pixfmt, size, fps) combos the device supports.
|
||||
* Filtered to MJPEG and YUYV. fmt_filter=0 accepts both.
|
||||
* Returns the count written to buf.
|
||||
*/
|
||||
static inline int v4l2_enumerate_formats(int fd, V4l2_Fmt_Option *buf, int buf_max,
|
||||
uint32_t fmt_filter)
|
||||
{
|
||||
V4l2_Opt_List l = { buf, 0, buf_max };
|
||||
|
||||
struct v4l2_fmtdesc fd_desc = {0};
|
||||
fd_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
|
||||
|
||||
for (fd_desc.index = 0;
|
||||
v4l2_xioctl(fd, VIDIOC_ENUM_FMT, &fd_desc) == 0;
|
||||
fd_desc.index++) {
|
||||
uint32_t pf = fd_desc.pixelformat;
|
||||
if (pf != V4L2_PIX_FMT_MJPEG && pf != V4L2_PIX_FMT_YUYV) { continue; }
|
||||
if (fmt_filter && pf != fmt_filter) { continue; }
|
||||
v4l2_collect_sizes(fd, pf, &l);
|
||||
}
|
||||
return l.n;
|
||||
}
|
||||
|
||||
/*
|
||||
* Select the best option from the list:
|
||||
* 1. Highest FPS
|
||||
* 2. Largest area (w×h) among equal-FPS entries
|
||||
* 3. MJPEG preferred over YUYV on equal FPS and area
|
||||
*/
|
||||
static inline const V4l2_Fmt_Option *v4l2_select_best(const V4l2_Fmt_Option *opts, int n)
|
||||
{
|
||||
if (n == 0) { return NULL; }
|
||||
const V4l2_Fmt_Option *best = &opts[0];
|
||||
for (int i = 1; i < n; i++) {
|
||||
const V4l2_Fmt_Option *o = &opts[i];
|
||||
if (v4l2_fmt_fps_gt(o, best)) {
|
||||
best = o;
|
||||
} else if (v4l2_fmt_fps_eq(o, best)) {
|
||||
int o_area = o->w * o->h;
|
||||
int b_area = best->w * best->h;
|
||||
if (o_area > b_area) {
|
||||
best = o;
|
||||
} else if (o_area == b_area &&
|
||||
o->pixfmt == V4L2_PIX_FMT_MJPEG &&
|
||||
best->pixfmt != V4L2_PIX_FMT_MJPEG) {
|
||||
best = o;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
@@ -82,6 +82,8 @@ Each module gets a corresponding CLI driver that exercises its API and serves as
|
||||
| `test_image_cli` | `test_image` | Generate test patterns, write PPM for visual inspection |
|
||||
| `xorg_cli` | `xorg` | Display test pattern in viewer window; exercises scale/anchor modes and text overlays |
|
||||
| `v4l2_view_cli` | V4L2 + `xorg` | Live camera viewer — auto-selects highest-FPS format, FPS/format overlay; bypasses node system |
|
||||
| `stream_send_cli` | V4L2 + `transport` + `protocol` | Capture MJPEG from V4L2, connect to receiver, send VIDEO_FRAME messages; prints fps/Mbps stats |
|
||||
| `stream_recv_cli` | `transport` + `protocol` + `xorg` | Listen for incoming VIDEO_FRAME stream, display in viewer; fps/Mbps overlay; threaded transport→GL handoff |
|
||||
|
||||
### Web UI (`dev/web/`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user