feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* framecache.c — Main entry point. HTTP API server + slot manager.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Endpoints:
|
|
|
|
|
|
* POST /slots Create slot
|
|
|
|
|
|
* GET /slots List slots
|
|
|
|
|
|
* GET /slots/:id Get slot detail
|
|
|
|
|
|
* DELETE /slots/:id Destroy slot
|
|
|
|
|
|
* GET /health Health check
|
|
|
|
|
|
*
|
|
|
|
|
|
* Uses libmicrohttpd for the HTTP layer (single-threaded, poll-based).
|
|
|
|
|
|
*/
|
|
|
|
|
|
#include "slot.h"
|
|
|
|
|
|
#include "registry.h"
|
2026-06-03 14:05:38 -04:00
|
|
|
|
#include <time.h>
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
#include <signal.h>
|
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
|
#include <microhttpd.h>
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef FC_PORT_DEFAULT
|
|
|
|
|
|
#define FC_PORT_DEFAULT 7435
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
/* ── tiny JSON helpers ─────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
static int json_get_uint(const char *json, const char *key, uint32_t *out)
|
|
|
|
|
|
{
|
|
|
|
|
|
char pat[128];
|
|
|
|
|
|
snprintf(pat, sizeof pat, "\"%s\":", key);
|
|
|
|
|
|
const char *p = strstr(json, pat);
|
|
|
|
|
|
if (!p) return -1;
|
|
|
|
|
|
p += strlen(pat);
|
|
|
|
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
|
|
|
|
*out = (uint32_t)strtoul(p, NULL, 10);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static int json_get_str(const char *json, const char *key,
|
|
|
|
|
|
char *out, size_t out_len)
|
|
|
|
|
|
{
|
|
|
|
|
|
char pat[128];
|
|
|
|
|
|
snprintf(pat, sizeof pat, "\"%s\":", key);
|
|
|
|
|
|
const char *p = strstr(json, pat);
|
|
|
|
|
|
if (!p) return -1;
|
|
|
|
|
|
p += strlen(pat);
|
|
|
|
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
|
|
|
|
if (*p != '"') return -1;
|
|
|
|
|
|
p++;
|
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
|
while (*p && *p != '"' && i < out_len - 1)
|
|
|
|
|
|
out[i++] = *p++;
|
|
|
|
|
|
out[i] = '\0';
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── HTTP request accumulator ──────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
|
char *buf;
|
|
|
|
|
|
size_t len;
|
|
|
|
|
|
size_t cap;
|
|
|
|
|
|
} req_body_t;
|
|
|
|
|
|
|
|
|
|
|
|
static void req_body_free(req_body_t *r)
|
|
|
|
|
|
{
|
|
|
|
|
|
free(r->buf);
|
|
|
|
|
|
r->buf = NULL; r->len = 0; r->cap = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── response helpers ──────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
static enum MHD_Result respond(struct MHD_Connection *conn,
|
|
|
|
|
|
unsigned int status,
|
|
|
|
|
|
const char *body)
|
|
|
|
|
|
{
|
|
|
|
|
|
struct MHD_Response *r = MHD_create_response_from_buffer(
|
|
|
|
|
|
strlen(body), (void *)body, MHD_RESPMEM_MUST_COPY);
|
|
|
|
|
|
MHD_add_response_header(r, "Content-Type", "application/json");
|
|
|
|
|
|
MHD_add_response_header(r, "Access-Control-Allow-Origin", "*");
|
|
|
|
|
|
enum MHD_Result rc = MHD_queue_response(conn, status, r);
|
|
|
|
|
|
MHD_destroy_response(r);
|
|
|
|
|
|
return rc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── slot → JSON ───────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
static void slot_to_json(struct fc_slot *s, char *buf, size_t len)
|
|
|
|
|
|
{
|
|
|
|
|
|
fc_header_t *hdr = fc_slot_header(s);
|
|
|
|
|
|
uint64_t wc = atomic_load(&hdr->write_cursor);
|
|
|
|
|
|
uint64_t df = atomic_load(&hdr->dropped_frames);
|
|
|
|
|
|
/* simple fps estimate — not perfect but good enough for status */
|
|
|
|
|
|
snprintf(buf, len,
|
|
|
|
|
|
"{"
|
|
|
|
|
|
"\"slot_id\":\"%s\","
|
|
|
|
|
|
"\"shm_path\":\"%s\","
|
|
|
|
|
|
"\"sem_name\":\"%s\","
|
|
|
|
|
|
"\"width\":%u,"
|
|
|
|
|
|
"\"height\":%u,"
|
|
|
|
|
|
"\"fps_num\":%u,"
|
|
|
|
|
|
"\"fps_den\":%u,"
|
|
|
|
|
|
"\"pixel_format\":\"UYVY422\","
|
|
|
|
|
|
"\"source_type\":\"%s\","
|
|
|
|
|
|
"\"frame_size\":%u,"
|
|
|
|
|
|
"\"ring_depth\":%u,"
|
|
|
|
|
|
"\"write_cursor\":%llu,"
|
|
|
|
|
|
"\"dropped_frames\":%llu"
|
|
|
|
|
|
"}",
|
|
|
|
|
|
fc_slot_id(s),
|
|
|
|
|
|
fc_slot_shm_path(s),
|
|
|
|
|
|
fc_slot_sem_name(s),
|
|
|
|
|
|
hdr->width, hdr->height,
|
|
|
|
|
|
hdr->fps_num, hdr->fps_den,
|
|
|
|
|
|
hdr->source_type,
|
|
|
|
|
|
hdr->frame_size,
|
|
|
|
|
|
hdr->ring_depth,
|
|
|
|
|
|
(unsigned long long)wc,
|
|
|
|
|
|
(unsigned long long)df
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── request handler ───────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
static enum MHD_Result handle_request(
|
|
|
|
|
|
void *cls,
|
|
|
|
|
|
struct MHD_Connection *conn,
|
|
|
|
|
|
const char *url,
|
|
|
|
|
|
const char *method,
|
|
|
|
|
|
const char *version,
|
|
|
|
|
|
const char *upload_data,
|
|
|
|
|
|
size_t *upload_data_size,
|
|
|
|
|
|
void **con_cls)
|
|
|
|
|
|
{
|
|
|
|
|
|
(void)cls; (void)version;
|
|
|
|
|
|
|
|
|
|
|
|
/* First call: allocate body accumulator */
|
|
|
|
|
|
if (*con_cls == NULL) {
|
|
|
|
|
|
req_body_t *rb = calloc(1, sizeof *rb);
|
|
|
|
|
|
if (!rb) return MHD_NO;
|
|
|
|
|
|
*con_cls = rb;
|
|
|
|
|
|
return MHD_YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
req_body_t *rb = (req_body_t *)*con_cls;
|
|
|
|
|
|
|
|
|
|
|
|
/* Accumulate POST body */
|
|
|
|
|
|
if (*upload_data_size > 0) {
|
|
|
|
|
|
size_t need = rb->len + *upload_data_size + 1;
|
|
|
|
|
|
if (need > rb->cap) {
|
|
|
|
|
|
rb->buf = realloc(rb->buf, need);
|
|
|
|
|
|
rb->cap = need;
|
|
|
|
|
|
}
|
|
|
|
|
|
memcpy(rb->buf + rb->len, upload_data, *upload_data_size);
|
|
|
|
|
|
rb->len += *upload_data_size;
|
|
|
|
|
|
rb->buf[rb->len] = '\0';
|
|
|
|
|
|
*upload_data_size = 0;
|
|
|
|
|
|
return MHD_YES;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
enum MHD_Result rc;
|
|
|
|
|
|
char resp[4096];
|
|
|
|
|
|
|
|
|
|
|
|
/* GET /health */
|
|
|
|
|
|
if (strcmp(method, "GET") == 0 && strcmp(url, "/health") == 0) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_OK, "{\"status\":\"ok\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 12:25:34 -04:00
|
|
|
|
/* GET /slots
|
|
|
|
|
|
* Worst case: FC_MAX_SLOTS (256) × ~2KB/entry ≈ 512KB. A 64KB stack buffer
|
|
|
|
|
|
* would overflow at ~32 slots (and `pos` could pass `sizeof big`, making
|
|
|
|
|
|
* `sizeof big - pos` underflow to a huge size_t). Heap-allocate a buffer
|
|
|
|
|
|
* sized for the worst case and bound-check every append. */
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
if (strcmp(method, "GET") == 0 && strcmp(url, "/slots") == 0) {
|
2026-06-03 12:25:34 -04:00
|
|
|
|
size_t cap = (size_t)FC_MAX_SLOTS * 2100 + 64; /* worst case + brackets */
|
|
|
|
|
|
char *big = malloc(cap);
|
|
|
|
|
|
if (!big) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
|
|
|
|
|
|
"{\"error\":\"out of memory\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
|
if (pos < cap) big[pos++] = '[';
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
int first = 1;
|
|
|
|
|
|
for (int i = 0; i < FC_MAX_SLOTS; i++) {
|
|
|
|
|
|
if (!g_registry[i].active) continue;
|
2026-06-03 12:25:34 -04:00
|
|
|
|
char entry[2100];
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
slot_to_json(g_registry[i].slot, entry, sizeof entry);
|
2026-06-03 12:25:34 -04:00
|
|
|
|
size_t elen = strlen(entry);
|
|
|
|
|
|
/* +2 for possible comma + closing bracket, +1 for NUL */
|
|
|
|
|
|
if (pos + elen + 3 >= cap) break; /* never overflow */
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
if (!first) big[pos++] = ',';
|
|
|
|
|
|
first = 0;
|
2026-06-03 12:25:34 -04:00
|
|
|
|
memcpy(big + pos, entry, elen);
|
|
|
|
|
|
pos += elen;
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
}
|
2026-06-03 12:25:34 -04:00
|
|
|
|
if (pos + 2 < cap) big[pos++] = ']';
|
|
|
|
|
|
big[pos] = '\0';
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
rc = respond(conn, MHD_HTTP_OK, big);
|
2026-06-03 12:25:34 -04:00
|
|
|
|
free(big);
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* GET /slots/:id */
|
|
|
|
|
|
if (strcmp(method, "GET") == 0 &&
|
|
|
|
|
|
strncmp(url, "/slots/", 7) == 0 && strlen(url) > 7)
|
|
|
|
|
|
{
|
|
|
|
|
|
const char *id = url + 7;
|
|
|
|
|
|
struct fc_slot *s = registry_find(id);
|
|
|
|
|
|
if (!s) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_NOT_FOUND,
|
|
|
|
|
|
"{\"error\":\"slot not found\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
slot_to_json(s, resp, sizeof resp);
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_OK, resp);
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* POST /slots */
|
|
|
|
|
|
if (strcmp(method, "POST") == 0 && strcmp(url, "/slots") == 0) {
|
|
|
|
|
|
if (!rb->buf || rb->len == 0) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_BAD_REQUEST,
|
|
|
|
|
|
"{\"error\":\"empty body\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
char slot_id[FC_MAX_SLOT_ID] = {0};
|
|
|
|
|
|
char source_type[32] = "unknown";
|
|
|
|
|
|
uint32_t width = 0, height = 0, fps_num = 0, fps_den = 0;
|
|
|
|
|
|
|
|
|
|
|
|
json_get_str(rb->buf, "slot_id", slot_id, sizeof slot_id);
|
|
|
|
|
|
json_get_str(rb->buf, "source_type", source_type, sizeof source_type);
|
|
|
|
|
|
json_get_uint(rb->buf, "width", &width);
|
|
|
|
|
|
json_get_uint(rb->buf, "height", &height);
|
|
|
|
|
|
json_get_uint(rb->buf, "fps_num", &fps_num);
|
|
|
|
|
|
json_get_uint(rb->buf, "fps_den", &fps_den);
|
|
|
|
|
|
|
|
|
|
|
|
if (!slot_id[0] || !width || !height || !fps_num || !fps_den) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_BAD_REQUEST,
|
|
|
|
|
|
"{\"error\":\"missing required fields: "
|
|
|
|
|
|
"slot_id, width, height, fps_num, fps_den\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (registry_find(slot_id)) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_CONFLICT,
|
|
|
|
|
|
"{\"error\":\"slot already exists\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
struct fc_slot *s = fc_slot_create(slot_id, width, height,
|
|
|
|
|
|
fps_num, fps_den,
|
|
|
|
|
|
FC_PIX_UYVY422, source_type);
|
|
|
|
|
|
if (!s) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
|
|
|
|
|
|
"{\"error\":\"failed to create slot\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
registry_add(s);
|
|
|
|
|
|
|
|
|
|
|
|
snprintf(resp, sizeof resp,
|
|
|
|
|
|
"{\"slot_id\":\"%s\","
|
|
|
|
|
|
"\"shm_path\":\"%s\","
|
|
|
|
|
|
"\"sem_name\":\"%s\"}",
|
|
|
|
|
|
fc_slot_id(s),
|
|
|
|
|
|
fc_slot_shm_path(s),
|
|
|
|
|
|
fc_slot_sem_name(s));
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_CREATED, resp);
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* DELETE /slots/:id */
|
|
|
|
|
|
if (strcmp(method, "DELETE") == 0 &&
|
|
|
|
|
|
strncmp(url, "/slots/", 7) == 0 && strlen(url) > 7)
|
|
|
|
|
|
{
|
|
|
|
|
|
const char *id = url + 7;
|
|
|
|
|
|
struct fc_slot *s = registry_find(id);
|
|
|
|
|
|
if (!s) {
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_NOT_FOUND,
|
|
|
|
|
|
"{\"error\":\"slot not found\"}");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
registry_remove(id);
|
|
|
|
|
|
fc_slot_destroy(s);
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_NO_CONTENT, "");
|
|
|
|
|
|
goto done;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
rc = respond(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"not found\"}");
|
|
|
|
|
|
|
|
|
|
|
|
done:
|
|
|
|
|
|
req_body_free(rb);
|
|
|
|
|
|
free(rb);
|
|
|
|
|
|
*con_cls = NULL;
|
|
|
|
|
|
return rc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void request_completed(void *cls,
|
|
|
|
|
|
struct MHD_Connection *conn,
|
|
|
|
|
|
void **con_cls,
|
|
|
|
|
|
enum MHD_RequestTerminationCode toe)
|
|
|
|
|
|
{
|
|
|
|
|
|
(void)cls; (void)conn; (void)toe;
|
|
|
|
|
|
if (*con_cls) {
|
|
|
|
|
|
req_body_free((req_body_t *)*con_cls);
|
|
|
|
|
|
free(*con_cls);
|
|
|
|
|
|
*con_cls = NULL;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ── main ──────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
static volatile int g_running = 1;
|
2026-06-03 16:05:55 -04:00
|
|
|
|
static volatile int g_received_signal = 0;
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
|
2026-06-03 16:05:55 -04:00
|
|
|
|
static void on_signal(int sig) { g_received_signal = sig; g_running = 0; }
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
|
|
|
|
|
|
int main(void)
|
|
|
|
|
|
{
|
|
|
|
|
|
signal(SIGINT, on_signal);
|
|
|
|
|
|
signal(SIGTERM, on_signal);
|
2026-06-03 16:05:55 -04:00
|
|
|
|
signal(SIGPIPE, SIG_IGN);
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
|
|
|
|
|
|
/* Ensure /dev/shm/framecache exists */
|
|
|
|
|
|
mkdir("/dev/shm/framecache", 0755);
|
|
|
|
|
|
|
|
|
|
|
|
/* Write empty registry */
|
|
|
|
|
|
registry_write_json();
|
|
|
|
|
|
|
|
|
|
|
|
const char *port_str = getenv("FC_PORT");
|
|
|
|
|
|
uint16_t port = port_str ? (uint16_t)atoi(port_str) : FC_PORT_DEFAULT;
|
|
|
|
|
|
|
|
|
|
|
|
struct MHD_Daemon *daemon = MHD_start_daemon(
|
|
|
|
|
|
MHD_USE_SELECT_INTERNALLY,
|
|
|
|
|
|
port,
|
|
|
|
|
|
NULL, NULL,
|
|
|
|
|
|
handle_request, NULL,
|
|
|
|
|
|
MHD_OPTION_NOTIFY_COMPLETED, request_completed, NULL,
|
|
|
|
|
|
MHD_OPTION_END);
|
|
|
|
|
|
|
|
|
|
|
|
if (!daemon) {
|
|
|
|
|
|
fprintf(stderr, "[framecache] failed to start HTTP server on port %u\n", port);
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fprintf(stderr, "[framecache] listening on port %u\n", port);
|
|
|
|
|
|
|
|
|
|
|
|
while (g_running) {
|
|
|
|
|
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100000000 }; /* 100ms */
|
|
|
|
|
|
nanosleep(&ts, NULL);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 16:05:55 -04:00
|
|
|
|
fprintf(stderr, "[framecache] shutting down (signal %d)\n", g_received_signal);
|
feat(framecache): phase 1 — framecache container + consumer library
- services/framecache/: new standalone container
- slot.h/slot.c: shm ring buffer (120 frames, FC_MAGIC header, atomic
write_cursor, POSIX semaphore per slot)
- registry.h/registry.c: in-memory slot registry + /dev/shm/framecache/
registry.json persistence
- framecache.c: HTTP API server (libmicrohttpd, port 7435)
POST /slots, GET /slots, GET /slots/:id, DELETE /slots/:id, GET /health
- fc_client.h/fc_client.c: consumer library — fc_consumer_open/read/close
with per-consumer cursor, timeout via sem_timedwait, automatic skip+count
when consumer falls behind writer by > ring_depth frames
- fc_test_consumer.c: dev utility to attach to any slot and print fps/stats
- CMakeLists.txt: framecache server + fc_client static lib + test consumer
- Dockerfile: builder + slim runtime stages
- docker-compose.worker.yml: add framecache service (profile: capture,
ipc: host, shm_size from FC_SHM_SIZE_GB env var, healthcheck)
- .env.example: document FC_SHM_SIZE_GB with per-node guidance
2026-06-03 10:53:51 -04:00
|
|
|
|
|
|
|
|
|
|
/* Destroy all active slots */
|
|
|
|
|
|
for (int i = 0; i < FC_MAX_SLOTS; i++) {
|
|
|
|
|
|
if (g_registry[i].active) {
|
|
|
|
|
|
registry_remove(g_registry[i].slot_id);
|
|
|
|
|
|
fc_slot_destroy(g_registry[i].slot);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MHD_stop_daemon(daemon);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|