# 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
