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:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user