# DeckLink Output Node — Architecture Plan ## What you'll build - `shared/DeckLinkSender.hpp` — analogous to `DeckLinkReceiver.hpp` - `nodes/decklinkout/main.cpp` + `CMakeLists.txt` ## The key difference: scheduled playback DeckLink output uses a **pull model** — the card calls you when it needs frames, rather than you reading when a frame arrives. Steps: 1. `EnableVideoOutput(mode, bmdVideoOutputFlagDefault)` 2. `EnableAudioOutput(48kHz, int32, channels, bmdAudioOutputStreamContinuous)` 3. Allocate a pool of ~3 `IDeckLinkMutableVideoFrame` objects via `CreateVideoFrame` 4. Pre-fill and `ScheduleVideoFrame` those 3 frames to prime the pipeline 5. `StartScheduledPlayback(0, fps_num, 1.0)` 6. The card fires `ScheduledFrameCompleted` callback when a frame has been displayed — fill it with the next grain and re-schedule it Audio is pushed separately: `ScheduleAudioSamples(buf, count, stream_time, timescale, &written)` — no callback, just keep it full. ## DeckLinkSender.hpp public API Use a condition variable the same way as the receiver, but inverted — the callback signals that a frame slot is free: ``` DeckLinkSender: start_output(device_index, width, height, fps_num, fps_den, channels) submit_frame(const uint8_t* src, uint32_t stride) // blocks until a slot is free submit_audio(const float* planar, int samples) // non-blocking push ``` ## Implementation steps 1. **`nodes/decklinkout/CMakeLists.txt`** — copy from `decklinkin`, rename target to `dmf-node-decklinkout` 2. **`shared/DeckLinkSender.hpp`** - `enumerate_devices()` — identical to receiver - `start_output()`: `EnableVideoOutput` → `EnableAudioOutput` → allocate 3 frames → pre-fill with black → `StartScheduledPlayback` - `OutputCallback` (nested private class) implementing `IDeckLinkVideoOutputCallback::ScheduledFrameCompleted` — pushes freed frame back to a queue, signals a CV - `submit_frame()`: waits for a free frame from the queue, `memcpy` src → frame, call `ScheduleVideoFrame` - `submit_audio()`: convert float32 planar → int32 interleaved (reverse of receiver), then call `ScheduleAudioSamples` 3. **`nodes/decklinkout/main.cpp`** — create MXL flow readers, detect format, create sender, loop reading grains and calling `submit_frame` / `submit_audio` ## Gotcha: stream time The `stream_time` passed to `ScheduleVideoFrame` must be monotonically increasing in units of `fps_num` (the timescale passed to `StartScheduledPlayback`). Easiest approach: keep a counter and pass `frame_count * fps_den` as the stream time. ```cpp ScheduleVideoFrame(frame, frame_count * fps_den, fps_den, fps_num); frame_count++; ```