221 lines
6.3 KiB
Markdown
221 lines
6.3 KiB
Markdown
# Unified Framecache — Implementation Plan
|
||
|
||
## Context
|
||
|
||
Replace the current named-FIFO-per-source architecture with a shared-memory
|
||
ring buffer (framecache) that fans raw video frames from any ingest source to
|
||
unlimited concurrent consumers with zero-copy reads.
|
||
|
||
**Approved design:** docs/design/framecache/DESIGN.md
|
||
**Branch:** feat/unified-framecache
|
||
**Roadmap (out of scope here):** RDMA cross-node, AJA, growing-file-while-recording browser playback
|
||
|
||
---
|
||
|
||
## Migration Strategy
|
||
|
||
Ship in 5 phases. Each phase is independently deployable and leaves the system
|
||
in a working state. Existing recording workflows are unaffected until Phase 5
|
||
cuts over.
|
||
|
||
---
|
||
|
||
## Phase 1 — Framecache Container (foundation)
|
||
|
||
**Goal:** Running framecache service with slot registry. No ingest writers yet.
|
||
|
||
### 1.1 — Create `services/framecache/` directory structure
|
||
|
||
```
|
||
services/framecache/
|
||
src/
|
||
framecache.c # main — slot manager + HTTP API
|
||
slot.c / slot.h # shm ring buffer lifecycle
|
||
registry.c # /dev/shm/framecache/registry.json writer
|
||
http.c # lightweight HTTP server (libmicrohttpd)
|
||
client/
|
||
fc_client.c / fc_client.h # consumer library
|
||
fc_client_node/
|
||
binding.cc # Node.js N-API addon
|
||
binding.gyp
|
||
Dockerfile
|
||
CMakeLists.txt
|
||
```
|
||
|
||
### 1.2 — Shared memory layout (slot.h)
|
||
|
||
Each slot lives at `/dev/shm/framecache/<slot_id>`:
|
||
|
||
```c
|
||
#define FC_MAGIC 0x46524D43 // "FRMC"
|
||
#define FC_RING_DEPTH 120 // ~2s at 59.94fps
|
||
#define FC_HEADER_SIZE 4096 // 4KB header block
|
||
|
||
typedef struct {
|
||
uint32_t magic;
|
||
uint32_t version; // = 1
|
||
uint32_t width;
|
||
uint32_t height;
|
||
uint32_t fps_num;
|
||
uint32_t fps_den;
|
||
uint32_t pixel_format; // FC_PIX_UYVY422 = 0
|
||
uint32_t frame_size; // width * height * 2
|
||
uint32_t ring_depth; // = FC_RING_DEPTH
|
||
_Atomic uint64_t write_cursor; // monotonically increasing frame index
|
||
_Atomic uint64_t dropped_frames;
|
||
uint8_t _pad[FC_HEADER_SIZE - 48];
|
||
} fc_header_t;
|
||
|
||
typedef struct {
|
||
uint64_t pts_us;
|
||
uint64_t wall_us;
|
||
uint32_t size;
|
||
uint8_t data[]; // frame_size bytes
|
||
} fc_frame_t;
|
||
```
|
||
|
||
Semaphore: `sem_open("/framecache-<slot_id>-write", ...)` — posted by writer
|
||
on each new frame, consumers `sem_timedwait` on it.
|
||
|
||
### 1.3 — HTTP API (port 7435)
|
||
|
||
```
|
||
POST /slots body: {slot_id, width, height, fps_num, fps_den, source_type}
|
||
creates shm region, writes registry entry
|
||
201 {slot_id, shm_path, sem_name}
|
||
|
||
GET /slots 200 [{slot_id, width, height, fps_num, fps_den,
|
||
source_type, write_cursor, dropped_frames,
|
||
current_fps}]
|
||
|
||
GET /slots/:id 200 slot detail
|
||
DELETE /slots/:id destroys shm + semaphore, removes registry entry, 204
|
||
GET /health 200 {status: "ok"}
|
||
```
|
||
|
||
### 1.4 — Registry file
|
||
|
||
Written to `/dev/shm/framecache/registry.json` on every slot create/delete.
|
||
|
||
### 1.5 — Dockerfile
|
||
|
||
```dockerfile
|
||
FROM debian:bookworm
|
||
RUN apt-get update && apt-get install -y \
|
||
build-essential cmake libmicrohttpd-dev \
|
||
&& rm -rf /var/lib/apt/lists/*
|
||
COPY . /src
|
||
RUN cmake -S /src -B /build -DCMAKE_BUILD_TYPE=Release \
|
||
&& cmake --build /build -j$(nproc)
|
||
EXPOSE 7435
|
||
CMD ["/build/framecache"]
|
||
```
|
||
|
||
### 1.6 — docker-compose.worker.yml addition
|
||
|
||
```yaml
|
||
framecache:
|
||
build: ./services/framecache
|
||
ipc: host
|
||
shm_size: '60gb'
|
||
environment:
|
||
FC_SHM_SIZE: ${FC_SHM_SIZE:-64424509440}
|
||
FC_PORT: 7435
|
||
ports:
|
||
- "7435:7435"
|
||
volumes:
|
||
- /dev/shm:/dev/shm
|
||
restart: unless-stopped
|
||
```
|
||
|
||
### 1.7 — Consumer library (fc_client.c)
|
||
|
||
```c
|
||
fc_slot_t *fc_open(const char *slot_id);
|
||
int fc_read_frame(fc_slot_t *slot, fc_frame_t **out, uint64_t timeout_ms);
|
||
void fc_close(fc_slot_t *slot);
|
||
```
|
||
|
||
**Commit:** `feat(framecache): phase 1 — framecache container + consumer library`
|
||
|
||
---
|
||
|
||
## Phase 2 — Deltacast Bridge writes to framecache
|
||
|
||
**Goal:** deltacast-bridge writes frames to framecache shm instead of named FIFOs.
|
||
Legacy FIFO path kept as compile-time fallback (`-DLEGACY_FIFO=ON`) until Phase 5.
|
||
|
||
On signal lock:
|
||
1. POST /slots to framecache HTTP API
|
||
2. shm_open + mmap the slot
|
||
3. Video thread writes frame into ring, advances write_cursor atomically, sem_post
|
||
4. Audio: keeps writing to audio FIFO (unchanged)
|
||
5. On shutdown: DELETE /slots/:id
|
||
|
||
**Commit:** `feat(framecache): phase 2 — deltacast-bridge writes to shm`
|
||
|
||
---
|
||
|
||
## Phase 3 — Blackmagic DeckLink Bridge
|
||
|
||
**Goal:** New decklink-bridge C program mirrors deltacast-bridge, replaces
|
||
ffmpeg -f decklink direct path.
|
||
|
||
- Uses IDeckLinkIterator to enumerate devices
|
||
- VideoInputFrameArrived callback calls fc_write_frame
|
||
- Registers slot on signal lock, deregisters on shutdown
|
||
- Audio stays in FIFO (same as deltacast)
|
||
|
||
**Commit:** `feat(framecache): phase 3 — decklink-bridge writes to shm`
|
||
|
||
---
|
||
|
||
## Phase 4 — capture-manager reads from framecache
|
||
|
||
**Goal:** Enables simultaneous growing + proxy + HLS from one SDI input.
|
||
|
||
- Node.js N-API addon wrapping fc_open/fc_read_frame/fc_close
|
||
- capture-manager opens THREE fc_client handles per slot (own cursor each):
|
||
1. Growing/master ffmpeg feed
|
||
2. Proxy ffmpeg feed
|
||
3. HLS preview ffmpeg feed
|
||
- Each gets a separate rawvideo pipe to ffmpeg
|
||
- Growing MXF workflow (raw2bmx orchestrator) completely unchanged
|
||
|
||
**Commit:** `feat(framecache): phase 4 — capture-manager reads from framecache`
|
||
|
||
---
|
||
|
||
## Phase 5 — Network ingest (RTMP/SRT) into framecache
|
||
|
||
**Goal:** RTMP and SRT sources decoded to raw UYVY422, written into framecache slots.
|
||
|
||
- net_ingest process per source: ffmpeg decodes to rawvideo, writes to slot
|
||
- capture-manager waits for slot, same fc_client consumer pattern
|
||
- Remove legacy FIFO code once all paths go through framecache
|
||
|
||
**Commit:** `feat(framecache): phase 5 — network ingest via framecache`
|
||
|
||
---
|
||
|
||
## Hardware / Deployment
|
||
|
||
| Node | RAM | /dev/shm | FC_SHM_SIZE |
|
||
|------|-----|----------|-------------|
|
||
| Baratheon | 251GB | 126GB | 60GB |
|
||
| zampp1 | 93GB | 47GB | 40GB |
|
||
| zampp2 | 18GB (upgrade) | 9.4GB | 8GB |
|
||
|
||
Ring buffer per 1080p59.94 source: ~494MB (120 frames × 4.1MB)
|
||
|
||
All recorder sidecars require `ipc: host`.
|
||
|
||
---
|
||
|
||
## Roadmap (not in this branch)
|
||
|
||
- Audio in framecache shm
|
||
- RDMA cross-node slot replication
|
||
- AJA hardware support
|
||
- Growing-file-while-recording browser HLS playback
|
||
- Mastercontrol/playout consumer
|