68 lines
2.7 KiB
Markdown
68 lines
2.7 KiB
Markdown
# 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++;
|
|
```
|