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.h — Framecache shared memory slot definitions.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Layout per slot (/dev/shm/framecache/<slot_id>):
|
|
|
|
|
|
* [fc_header_t — 4KB aligned]
|
|
|
|
|
|
* [fc_frame_t × ring_depth — each FC_FRAME_HDR_SIZE + frame_size bytes]
|
|
|
|
|
|
*
|
|
|
|
|
|
* Writer advances write_cursor atomically and posts the named semaphore.
|
|
|
|
|
|
* Each consumer tracks its own read_cursor independently — writer never blocks.
|
|
|
|
|
|
*/
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
|
|
#include <stdint.h>
|
|
|
|
|
|
#include <stdatomic.h>
|
|
|
|
|
|
#include <semaphore.h>
|
|
|
|
|
|
|
|
|
|
|
|
#define FC_MAGIC 0x46524D43u /* "FRMC" */
|
|
|
|
|
|
#define FC_VERSION 1u
|
|
|
|
|
|
#define FC_RING_DEPTH 120u /* ~2s at 59.94fps */
|
|
|
|
|
|
#define FC_HEADER_SIZE 4096u /* 4KB header block */
|
|
|
|
|
|
#define FC_FRAME_HDR_SIZE 24u /* pts_us(8) + wall_us(8) + size(4) + pad(4) */
|
|
|
|
|
|
#define FC_MAX_SLOT_ID 64u
|
|
|
|
|
|
|
|
|
|
|
|
/* Pixel format codes */
|
|
|
|
|
|
#define FC_PIX_UYVY422 0u
|
|
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
|
uint32_t magic; /* FC_MAGIC */
|
|
|
|
|
|
uint32_t version; /* FC_VERSION */
|
|
|
|
|
|
uint32_t width;
|
|
|
|
|
|
uint32_t height;
|
|
|
|
|
|
uint32_t fps_num;
|
|
|
|
|
|
uint32_t fps_den;
|
|
|
|
|
|
uint32_t pixel_format; /* FC_PIX_UYVY422 */
|
|
|
|
|
|
uint32_t frame_size; /* width * height * 2 */
|
|
|
|
|
|
uint32_t ring_depth; /* FC_RING_DEPTH */
|
|
|
|
|
|
uint32_t _reserved;
|
|
|
|
|
|
_Atomic uint64_t write_cursor; /* monotonically increasing frame index */
|
|
|
|
|
|
_Atomic uint64_t dropped_frames;
|
|
|
|
|
|
char source_type[32]; /* "deltacast" | "blackmagic" | "srt" | "rtmp" */
|
|
|
|
|
|
char slot_id[FC_MAX_SLOT_ID];
|
2026-06-03 14:08:28 -04:00
|
|
|
|
uint8_t _pad[FC_HEADER_SIZE - 144];
|
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
|
|
|
|
} fc_header_t;
|
|
|
|
|
|
|
|
|
|
|
|
/* Per-frame metadata + data (variable length — use fc_frame_at() accessor) */
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
|
uint64_t pts_us;
|
|
|
|
|
|
uint64_t wall_us;
|
|
|
|
|
|
uint32_t size;
|
|
|
|
|
|
uint32_t _pad;
|
|
|
|
|
|
uint8_t data[]; /* frame_size bytes */
|
|
|
|
|
|
} fc_frame_t;
|
|
|
|
|
|
|
|
|
|
|
|
/* Compile-time size check */
|
|
|
|
|
|
_Static_assert(sizeof(fc_header_t) == FC_HEADER_SIZE,
|
|
|
|
|
|
"fc_header_t must be exactly FC_HEADER_SIZE bytes");
|
|
|
|
|
|
_Static_assert(sizeof(fc_frame_t) == FC_FRAME_HDR_SIZE,
|
|
|
|
|
|
"fc_frame_t header must be exactly FC_FRAME_HDR_SIZE bytes");
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Compute total shm size for a slot given frame_size.
|
|
|
|
|
|
* = FC_HEADER_SIZE + ring_depth * (FC_FRAME_HDR_SIZE + frame_size)
|
|
|
|
|
|
*/
|
|
|
|
|
|
static inline size_t fc_slot_shm_size(uint32_t frame_size) {
|
|
|
|
|
|
return (size_t)FC_HEADER_SIZE
|
|
|
|
|
|
+ (size_t)FC_RING_DEPTH * ((size_t)FC_FRAME_HDR_SIZE + frame_size);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Return pointer to frame at ring index idx within a mapped shm base.
|
|
|
|
|
|
*/
|
|
|
|
|
|
static inline fc_frame_t *fc_frame_at(void *base, uint32_t frame_size, uint64_t idx) {
|
|
|
|
|
|
uint8_t *frames = (uint8_t *)base + FC_HEADER_SIZE;
|
|
|
|
|
|
return (fc_frame_t *)(frames + (idx % FC_RING_DEPTH)
|
|
|
|
|
|
* ((size_t)FC_FRAME_HDR_SIZE + frame_size));
|
|
|
|
|
|
}
|