Add protocol module, video-node binary, query/web CLI tools

- Protocol module: framed binary encoding for control requests/responses
  (ENUM_DEVICES, ENUM_CONTROLS, GET/SET_CONTROL, STREAM_OPEN/CLOSE)
- video-node: scans /dev/media* and /dev/video*, serves V4L2 device
  topology and controls over TCP; uses UDP discovery for peer announce
- query_cli: auto-discovers a node, queries devices and controls
- protocol_cli: low-level protocol frame decoder for debugging
- dev/web: Express 5 ESM web inspector — live SSE discovery picker,
  REST bridge to video-node, controls UI with sliders/selects/checkboxes
- Makefile: sequential module builds before cli/node to fix make -j races
- common.mk: add DEPFLAGS (-MMD -MP) for automatic header dependencies
- All module Makefiles: split compile/link, generate .d dependency files
- discovery: replace 100ms poll loop with pthread_cond_timedwait;
  respond to all announcements (not just new peers) for instant re-discovery
- ENUM_DEVICES response: carry device_caps (V4L2_CAP_*) per video node
  so clients can distinguish capture nodes from metadata nodes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 01:04:56 +00:00
parent 34386b635e
commit 62c25247ef
32 changed files with 3998 additions and 81 deletions

404
include/protocol.h Normal file
View File

@@ -0,0 +1,404 @@
#pragma once
#include <stdint.h>
#include "error.h"
#include "transport.h"
/* -------------------------------------------------------------------------
* Message type constants
* ------------------------------------------------------------------------- */
#define PROTO_MSG_VIDEO_FRAME 0x0001u
#define PROTO_MSG_CONTROL_REQUEST 0x0002u
#define PROTO_MSG_CONTROL_RESPONSE 0x0003u
#define PROTO_MSG_STREAM_EVENT 0x0004u
/* -------------------------------------------------------------------------
* Command codes (carried in CONTROL_REQUEST payload offset 2)
* ------------------------------------------------------------------------- */
#define PROTO_CMD_STREAM_OPEN 0x0001u
#define PROTO_CMD_STREAM_CLOSE 0x0002u
#define PROTO_CMD_ENUM_DEVICES 0x0003u
#define PROTO_CMD_ENUM_CONTROLS 0x0004u
#define PROTO_CMD_GET_CONTROL 0x0005u
#define PROTO_CMD_SET_CONTROL 0x0006u
#define PROTO_CMD_ENUM_MONITORS 0x0007u
/* -------------------------------------------------------------------------
* Response status codes (carried in CONTROL_RESPONSE payload offset 2)
* ------------------------------------------------------------------------- */
#define PROTO_STATUS_OK 0x0000u
#define PROTO_STATUS_ERROR 0x0001u
#define PROTO_STATUS_UNKNOWN_CMD 0x0002u
#define PROTO_STATUS_INVALID_PARAMS 0x0003u
#define PROTO_STATUS_NOT_FOUND 0x0004u
/* -------------------------------------------------------------------------
* Stream event codes (carried in STREAM_EVENT payload offset 2)
* ------------------------------------------------------------------------- */
#define PROTO_EVENT_INTERRUPTED 0x01u
#define PROTO_EVENT_RESUMED 0x02u
/* -------------------------------------------------------------------------
* Codec format codes (STREAM_OPEN format field)
* ------------------------------------------------------------------------- */
#define PROTO_FORMAT_MJPEG 0x0001u
#define PROTO_FORMAT_H264 0x0002u
#define PROTO_FORMAT_H265 0x0003u
#define PROTO_FORMAT_AV1 0x0004u
#define PROTO_FORMAT_FFV1 0x0005u
#define PROTO_FORMAT_PRORES 0x0006u
#define PROTO_FORMAT_QOI 0x0007u
#define PROTO_FORMAT_RAW 0x0008u
#define PROTO_FORMAT_RAW_ZSTD 0x0009u
/* -------------------------------------------------------------------------
* Pixel format codes (STREAM_OPEN pixel_format field; 0 for compressed)
* ------------------------------------------------------------------------- */
#define PROTO_PIXEL_BGRA8888 0x0001u
#define PROTO_PIXEL_RGBA8888 0x0002u
#define PROTO_PIXEL_BGR888 0x0003u
#define PROTO_PIXEL_YUV420P 0x0004u
#define PROTO_PIXEL_YUV422 0x0005u
/* -------------------------------------------------------------------------
* Origin codes (STREAM_OPEN origin field; informational only)
* ------------------------------------------------------------------------- */
#define PROTO_ORIGIN_DEVICE_NATIVE 0x0001u
#define PROTO_ORIGIN_LIBJPEG_TURBO 0x0002u
#define PROTO_ORIGIN_FFMPEG_LIBAV 0x0003u
#define PROTO_ORIGIN_FFMPEG_PROC 0x0004u
#define PROTO_ORIGIN_VAAPI 0x0005u
#define PROTO_ORIGIN_NVENC 0x0006u
#define PROTO_ORIGIN_SOFTWARE 0x0007u
/* -------------------------------------------------------------------------
* Structs used by write functions (variable-length response payloads)
* ------------------------------------------------------------------------- */
struct Proto_Menu_Item {
uint32_t index;
const char *name;
int64_t int_value;
};
struct Proto_Control_Info {
uint32_t id;
uint8_t type;
uint32_t flags;
const char *name;
int32_t min, max, step, default_val, current_val;
uint8_t menu_count;
const struct Proto_Menu_Item *menu_items;
};
/*
* A video node associated with a media controller device.
* entity_type and entity_flags are MEDIA_ENT_F_* / MEDIA_ENT_FL_* values.
* pad_flags uses MEDIA_PAD_FLAG_SOURCE / MEDIA_PAD_FLAG_SINK.
* is_capture: 1 if this node is the primary video capture output.
*/
struct Proto_Video_Node_Info {
const char *path;
const char *entity_name;
uint32_t entity_type;
uint32_t entity_flags;
uint32_t device_caps; /* V4L2_CAP_* bits from VIDIOC_QUERYCAP */
uint8_t pad_flags;
uint8_t is_capture;
};
/*
* A media controller device and its associated video nodes.
* video_node_count must be <= 255.
*/
struct Proto_Media_Device_Info {
const char *path;
const char *driver;
const char *model;
const char *bus_info;
uint8_t video_node_count;
const struct Proto_Video_Node_Info *video_nodes;
};
/*
* A standalone V4L2 device with no associated media controller.
* name is the card name from VIDIOC_QUERYCAP.
*/
struct Proto_Standalone_Device_Info {
const char *path;
const char *name;
};
struct Proto_Monitor_Info {
int32_t x, y;
uint32_t width, height;
const char *name;
};
/* -------------------------------------------------------------------------
* Structs used by read functions
* Fields with pointer types point INTO the caller's payload buffer.
* The caller must keep the payload alive while using those pointers.
* Strings are NOT NUL-terminated.
* ------------------------------------------------------------------------- */
struct Proto_Video_Frame {
uint16_t stream_id;
const uint8_t *data;
uint32_t data_len;
};
struct Proto_Stream_Event {
uint16_t stream_id;
uint8_t event_code;
};
struct Proto_Request_Header {
uint16_t request_id;
uint16_t command;
};
struct Proto_Stream_Open {
uint16_t request_id;
uint16_t stream_id;
uint16_t format;
uint16_t pixel_format;
uint16_t origin;
};
struct Proto_Stream_Close {
uint16_t request_id;
uint16_t stream_id;
};
struct Proto_Enum_Controls_Req {
uint16_t request_id;
uint16_t device_index;
};
struct Proto_Get_Control_Req {
uint16_t request_id;
uint16_t device_index;
uint32_t control_id;
};
struct Proto_Set_Control_Req {
uint16_t request_id;
uint16_t device_index;
uint32_t control_id;
int32_t value;
};
struct Proto_Response_Header {
uint16_t request_id;
uint16_t status;
};
struct Proto_Get_Control_Resp {
uint16_t request_id;
uint16_t status;
int32_t value;
};
/* -------------------------------------------------------------------------
* Write functions — serialize and send via transport_send_frame.
* All return APP_OK or an error from the transport layer.
* ------------------------------------------------------------------------- */
/*
* VIDEO_FRAME: prepends stream_id (2 bytes) to data and sends.
* data/data_len is the compressed frame; the stream must already be open.
*/
struct App_Error proto_write_video_frame(struct Transport_Conn *conn,
uint16_t stream_id, const uint8_t *data, uint32_t data_len);
/* STREAM_EVENT (3 bytes) */
struct App_Error proto_write_stream_event(struct Transport_Conn *conn,
uint16_t stream_id, uint8_t event_code);
/* CONTROL_REQUEST: STREAM_OPEN (12 bytes) */
struct App_Error proto_write_stream_open(struct Transport_Conn *conn,
uint16_t request_id, uint16_t stream_id,
uint16_t format, uint16_t pixel_format, uint16_t origin);
/* CONTROL_REQUEST: STREAM_CLOSE (6 bytes) */
struct App_Error proto_write_stream_close(struct Transport_Conn *conn,
uint16_t request_id, uint16_t stream_id);
/* CONTROL_REQUEST: ENUM_DEVICES (4 bytes, no extra fields) */
struct App_Error proto_write_enum_devices(struct Transport_Conn *conn,
uint16_t request_id);
/* CONTROL_REQUEST: ENUM_CONTROLS (6 bytes) */
struct App_Error proto_write_enum_controls(struct Transport_Conn *conn,
uint16_t request_id, uint16_t device_index);
/* CONTROL_REQUEST: GET_CONTROL (10 bytes) */
struct App_Error proto_write_get_control(struct Transport_Conn *conn,
uint16_t request_id, uint16_t device_index, uint32_t control_id);
/* CONTROL_REQUEST: SET_CONTROL (14 bytes) */
struct App_Error proto_write_set_control(struct Transport_Conn *conn,
uint16_t request_id, uint16_t device_index,
uint32_t control_id, int32_t value);
/* CONTROL_REQUEST: ENUM_MONITORS (4 bytes, no extra fields) */
struct App_Error proto_write_enum_monitors(struct Transport_Conn *conn,
uint16_t request_id);
/*
* CONTROL_RESPONSE: generic.
* payload/payload_len are the command-specific bytes after request_id+status.
* Pass payload=NULL, payload_len=0 for responses with no extra fields
* (e.g. STREAM_OPEN ok, STREAM_CLOSE, SET_CONTROL).
*/
struct App_Error proto_write_control_response(struct Transport_Conn *conn,
uint16_t request_id, uint16_t status,
const uint8_t *payload, uint32_t payload_len);
/* CONTROL_RESPONSE: ENUM_DEVICES */
struct App_Error proto_write_enum_devices_response(struct Transport_Conn *conn,
uint16_t request_id, uint16_t status,
const struct Proto_Media_Device_Info *media_devices, uint16_t media_count,
const struct Proto_Standalone_Device_Info *standalone, uint16_t standalone_count);
/* CONTROL_RESPONSE: ENUM_CONTROLS */
struct App_Error proto_write_enum_controls_response(struct Transport_Conn *conn,
uint16_t request_id, uint16_t status,
const struct Proto_Control_Info *controls, uint16_t count);
/* CONTROL_RESPONSE: GET_CONTROL */
struct App_Error proto_write_get_control_response(struct Transport_Conn *conn,
uint16_t request_id, uint16_t status, int32_t value);
/* CONTROL_RESPONSE: ENUM_MONITORS */
struct App_Error proto_write_enum_monitors_response(struct Transport_Conn *conn,
uint16_t request_id, uint16_t status,
const struct Proto_Monitor_Info *monitors, uint16_t count);
/* -------------------------------------------------------------------------
* Read functions — parse raw payload bytes into typed structs.
* All return APP_INVALID_ERROR_MSG(0, ...) on malformed payloads.
* ------------------------------------------------------------------------- */
struct App_Error proto_read_video_frame(
const uint8_t *payload, uint32_t length,
struct Proto_Video_Frame *out);
struct App_Error proto_read_stream_event(
const uint8_t *payload, uint32_t length,
struct Proto_Stream_Event *out);
/*
* Read the common 4-byte request header (request_id + command).
* Dispatch on header.command, then call the appropriate specific reader.
* ENUM_DEVICES and ENUM_MONITORS have no extra fields beyond the header.
*/
struct App_Error proto_read_request_header(
const uint8_t *payload, uint32_t length,
struct Proto_Request_Header *out);
struct App_Error proto_read_stream_open(
const uint8_t *payload, uint32_t length,
struct Proto_Stream_Open *out);
struct App_Error proto_read_stream_close(
const uint8_t *payload, uint32_t length,
struct Proto_Stream_Close *out);
struct App_Error proto_read_enum_controls_req(
const uint8_t *payload, uint32_t length,
struct Proto_Enum_Controls_Req *out);
struct App_Error proto_read_get_control_req(
const uint8_t *payload, uint32_t length,
struct Proto_Get_Control_Req *out);
struct App_Error proto_read_set_control_req(
const uint8_t *payload, uint32_t length,
struct Proto_Set_Control_Req *out);
/*
* Read the common 4-byte response header (request_id + status).
* For responses with no extra fields (STREAM_OPEN, STREAM_CLOSE, SET_CONTROL),
* this is the complete parse.
*/
struct App_Error proto_read_response_header(
const uint8_t *payload, uint32_t length,
struct Proto_Response_Header *out);
struct App_Error proto_read_get_control_response(
const uint8_t *payload, uint32_t length,
struct Proto_Get_Control_Resp *out);
/*
* Variable-length response readers use callbacks to avoid heap allocation.
* Strings point into the payload and are NOT NUL-terminated; use *_len.
*/
/*
* on_media_device is called once per media controller device.
* on_video_node is called video_node_count times immediately after,
* once per video node belonging to that media device.
* on_standalone is called once per V4L2 device with no media controller.
* Any callback may be NULL.
*/
struct App_Error proto_read_enum_devices_response(
const uint8_t *payload, uint32_t length,
struct Proto_Response_Header *header_out,
void (*on_media_device)(
const char *path, uint8_t path_len,
const char *driver, uint8_t driver_len,
const char *model, uint8_t model_len,
const char *bus_info, uint8_t bus_info_len,
uint8_t video_node_count,
void *userdata),
void (*on_video_node)(
const char *path, uint8_t path_len,
const char *entity_name, uint8_t entity_name_len,
uint32_t entity_type, uint32_t entity_flags,
uint32_t device_caps,
uint8_t pad_flags, uint8_t is_capture,
void *userdata),
void (*on_standalone)(
const char *path, uint8_t path_len,
const char *name, uint8_t name_len,
void *userdata),
void *userdata);
/*
* on_control is called once per control.
* on_menu_item is called once per menu item immediately after its on_control
* call; menu_count in on_control says how many to expect.
* on_menu_item may be NULL if the caller does not need menu items.
*/
struct App_Error proto_read_enum_controls_response(
const uint8_t *payload, uint32_t length,
struct Proto_Response_Header *header_out,
void (*on_control)(
uint32_t id, uint8_t type, uint32_t flags,
const char *name, uint8_t name_len,
int32_t min, int32_t max, int32_t step,
int32_t default_val, int32_t current_val,
uint8_t menu_count, void *userdata),
void (*on_menu_item)(
uint32_t index,
const char *name, uint8_t name_len,
int64_t int_value,
void *userdata),
void *userdata);
struct App_Error proto_read_enum_monitors_response(
const uint8_t *payload, uint32_t length,
struct Proto_Response_Header *header_out,
void (*on_monitor)(
int32_t x, int32_t y, uint32_t width, uint32_t height,
const char *name, uint8_t name_len,
void *userdata),
void *userdata);