diff --git a/decklinkout-plan.md b/decklinkout-plan.md new file mode 100644 index 0000000..0f3c24b --- /dev/null +++ b/decklinkout-plan.md @@ -0,0 +1,67 @@ +# 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++; +```