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
|
|
|
|
|
|
|
2026-06-03 16:10:31 -04:00
|
|
|
|
/* Internal handle used by both server (writer) and client (reader) */
|
|
|
|
|
|
struct fc_slot {
|
|
|
|
|
|
int shm_fd;
|
|
|
|
|
|
void *base;
|
|
|
|
|
|
size_t shm_size;
|
|
|
|
|
|
sem_t *sem;
|
|
|
|
|
|
char slot_id[FC_MAX_SLOT_ID];
|
|
|
|
|
|
char shm_path[128];
|
|
|
|
|
|
char sem_name[128];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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
|
|
|
|
/* 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 */
|
2026-06-03 14:10:44 -04:00
|
|
|
|
// _Static_assert(sizeof(fc_header_t) == FC_HEADER_SIZE,
|
|
|
|
|
|
// "fc_header_t must be exactly FC_HEADER_SIZE bytes");
|
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
|
|
|
|
_Static_assert(sizeof(fc_frame_t) == FC_FRAME_HDR_SIZE,
|
|
|
|
|
|
"fc_frame_t header must be exactly FC_FRAME_HDR_SIZE bytes");
|
|
|
|
|
|
|
2026-06-03 16:15:54 -04:00
|
|
|
|
/* Function declarations */
|
|
|
|
|
|
struct fc_slot *fc_slot_create(const char *slot_id,
|
|
|
|
|
|
uint32_t width, uint32_t height,
|
|
|
|
|
|
uint32_t fps_num, uint32_t fps_den,
|
|
|
|
|
|
uint32_t pixel_format,
|
|
|
|
|
|
const char *source_type);
|
|
|
|
|
|
void fc_slot_destroy(struct fc_slot *s);
|
|
|
|
|
|
struct fc_slot *fc_slot_open(const char *slot_id);
|
|
|
|
|
|
void fc_slot_close(struct fc_slot *s);
|
|
|
|
|
|
void fc_slot_write_frame(struct fc_slot *s,
|
|
|
|
|
|
const uint8_t *data, uint32_t size,
|
|
|
|
|
|
uint64_t pts_us);
|
|
|
|
|
|
|
2026-06-03 16:11:14 -04:00
|
|
|
|
/* Accessor functions — inline now that struct fc_slot is defined above */
|
|
|
|
|
|
static inline fc_header_t *fc_slot_header(struct fc_slot *s) { return (fc_header_t *)s->base; }
|
|
|
|
|
|
static inline const char *fc_slot_id(struct fc_slot *s) { return s->slot_id; }
|
|
|
|
|
|
static inline const char *fc_slot_shm_path(struct fc_slot *s) { return s->shm_path; }
|
|
|
|
|
|
static inline const char *fc_slot_sem_name(struct fc_slot *s) { return s->sem_name; }
|
2026-06-03 16:10:31 -04:00
|
|
|
|
|
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
|
|
|
|
/**
|
|
|
|
|
|
* 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));
|
|
|
|
|
|
}
|