Files
dmf-studio-rnd/shared/DeckLinkSender.hpp
T
2026-07-07 16:46:03 +03:00

250 lines
9.8 KiB
C++

#pragma once
#include <atomic>
#include <stdexcept>
#include <string>
#include <vector>
#include <DeckLinkAPI.h>
#include "V210.hpp"
namespace dmf {
class DeckLinkSender {
public:
struct VideoInfo {
int width = 0;
int height = 0;
int fps_num = 0;
int fps_den = 0;
};
struct AudioInfo {
int sample_rate = 48000;
int channels = 0;
};
struct DeviceInfo {
uint32_t index;
std::string name;
};
std::vector<DeviceInfo> devices;
VideoInfo video_info{};
AudioInfo audio_info{};
bool has_audio = false;
int64_t frame_count = 0;
std::atomic<int64_t> audio_stream_time{0};
DeckLinkSender() { enumerate_devices(); }
~DeckLinkSender() {
if (decklink_output) {
decklink_output->StopScheduledPlayback(frame_count * video_info.fps_den,
nullptr,
video_info.fps_num);
decklink_output->DisableVideoOutput();
if (has_audio) decklink_output->DisableAudioOutput();;
decklink_output->SetScheduledFrameCompletionCallback(nullptr);
decklink_output->Release();
for (auto* vf : frame_pool) vf->Release();
frame_pool.clear();
}
delete output_callback;
for (auto* d : raw_devices) d->Release();
if (selected_device) selected_device->Release();
}
// channels > 0 enables audio output at 48 kHz / 32-bit int.
void start_output(uint32_t device_index, int width, int height, int fps_num, int fps_den, int channels = 0) {
if (device_index >= raw_devices.size())
throw std::runtime_error("Device index out of range");
video_info.width = width;
video_info.height = height;
video_info.fps_num = fps_num;
video_info.fps_den = fps_den;
BMDDisplayMode bm_display_mode = bmdModeHD1080p25; //need to create a func for detection
selected_device = raw_devices[device_index];
selected_device->AddRef();
HRESULT r = selected_device->QueryInterface(IID_IDeckLinkOutput, (void**)&decklink_output);
if (r != S_OK) throw std::runtime_error("Could not obtain IDeckLinkOutput");
output_callback = new OutputCallback(*this);
r = decklink_output->SetScheduledFrameCompletionCallback(output_callback);
if (r != S_OK) throw std::runtime_error("Could not set output callback");
r = decklink_output->EnableVideoOutput(bmdModeHD1080p25, bmdVideoOutputFlagDefault);
if (r != S_OK) throw std::runtime_error("Could not enable video output");
if (channels > 0) {
r = decklink_output->EnableAudioOutput(bmdAudioSampleRate48kHz,
bmdAudioSampleType32bitInteger,
static_cast<uint32_t>(channels),
bmdAudioOutputStreamTimestamped);
if (r != S_OK) throw std::runtime_error("Could not enable audio output");
// r = decklink_output->SetAudioCallback
has_audio = true;
audio_info.channels = channels;
}
bool is_supported = false;
BMDDisplayMode actual_mode;
r = decklink_output->DoesSupportVideoMode(
bmdVideoConnectionUnspecified, // TODO: create selection between sdi, hdmi, etc.
bmdModeHD1080p25,
bmdFormat10BitYUV,
bmdNoVideoOutputConversion,
bmdSupportedVideoModeDefault,
&actual_mode,
&is_supported
);
if (r != S_OK) throw std::runtime_error("Selected mode is not supported for bmdFormat10BitYUV");
int32_t row_bytes;
r = decklink_output->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &row_bytes);
if (r != S_OK) throw std::runtime_error("Could not get row bytes for display mode");
// prefill frame pool
const int64_t duration = fps_den;
const int64_t timescale = fps_num;
const size_t preroll_pool_size = 3; // min=3, cause 1 displaying, 1 queued, 1 writable
for (size_t i = 0; i < preroll_pool_size; ++i) {
IDeckLinkMutableVideoFrame* vf = nullptr;
r = decklink_output->CreateVideoFrame(width, height, row_bytes, bmdFormat10BitYUV, bmdFrameFlagDefault, &vf);
if (r != S_OK) throw std::runtime_error("Could not create a video frame");
IDeckLinkVideoBuffer* buf = nullptr;
vf->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf);
buf->StartAccess(bmdBufferAccessWrite);
void* ptr = nullptr;
buf->GetBytes(&ptr);
if (ptr) dmf::v210::fill_black(static_cast<uint8_t*>(ptr), width, height, row_bytes);
buf->EndAccess(bmdBufferAccessWrite);
buf->Release();
decklink_output->ScheduleVideoFrame(vf, i * duration, duration, timescale);
frame_pool.push_back(vf);
}
output_callback->next_time.store(preroll_pool_size * duration);
// start playback
r = decklink_output->StartScheduledPlayback(0, timescale, 1.0);
if (r != S_OK) throw std::runtime_error("Could not start streams");
}
void submit_frame(const uint8_t* src, uint32_t stride) {
// Grab a free frame from the pool (round-robin)
IDeckLinkMutableVideoFrame* vf = frame_pool[frame_count % frame_pool.size()];
frame_count++;
IDeckLinkVideoBuffer* buf = nullptr;
if (vf->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) != S_OK) return;
buf->StartAccess(bmdBufferAccessWrite);
void* ptr = nullptr;
buf->GetBytes(&ptr);
if (ptr) {
// Row-by-row copy with stride adaptation
const uint32_t dst_stride = vf->GetRowBytes();
const uint32_t copy_row = std::min(stride, dst_stride);
for (int y = 0; y < video_info.height; ++y) {
std::memcpy(static_cast<uint8_t*>(ptr) + y * dst_stride,
src + y * stride, copy_row);
}
}
buf->EndAccess(bmdBufferAccessWrite);
buf->Release();
// Schedule for display
BMDTimeValue display_time = output_callback->next_time.fetch_add(video_info.fps_den);
decklink_output->ScheduleVideoFrame(vf, display_time,
video_info.fps_den, video_info.fps_num);
}
void submit_audio(const float* planar, int samples) {
if (!has_audio || samples <= 0) return;
// float32 planar → interleaved int32 (4-byte PCM) scaled to int32 range
const int channels = audio_info.channels;
std::vector<int32_t> interleaved(
static_cast<size_t>(channels) * static_cast<size_t>(samples));
for (int s = 0; s < samples; ++s) {
for (int c = 0; c < channels; ++c) {
float v = planar[c * samples + s]; // planar: channel-major
// clamp to safe range and scale
if (v > 1.0f) v = 1.0f;
else if (v < -1.0f) v = -1.0f;
interleaved[static_cast<size_t>(s) * channels + c] =
static_cast<int32_t>(v * 2147483647.0f);
}
}
const int64_t stream_time = audio_stream_time.fetch_add(samples);
uint32_t written = 0;
decklink_output->ScheduleAudioSamples(
interleaved.data(), static_cast<uint32_t>(samples),
stream_time, audio_info.sample_rate, &written);
(void)written;
}
private:
class OutputCallback: public IDeckLinkVideoOutputCallback {
public:
explicit OutputCallback(DeckLinkSender& owner) : owner(owner) {}
HRESULT ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result) override {
// Frame is done displaying — return it to the pool.
// submit_frame will overwrite and reschedule it with fresh MXL data.
return S_OK;
}
HRESULT ScheduledPlaybackHasStopped (void) override {
return S_OK;
}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, LPVOID*) override { return E_NOINTERFACE; }
ULONG STDMETHODCALLTYPE AddRef() override { return ++ref_count; }
ULONG STDMETHODCALLTYPE Release() override { return --ref_count; }
std::atomic<int64_t> next_time{0};
private:
DeckLinkSender& owner;
std::atomic<uint64_t> next_frame_idx{0};
std::atomic<int32_t> ref_count{1};
};
// DeckLink SDK objects
std::vector<IDeckLink*> raw_devices;
IDeckLink* selected_device = nullptr;
IDeckLinkOutput* decklink_output = nullptr;
OutputCallback* output_callback = nullptr;
std::vector<IDeckLinkMutableVideoFrame*> frame_pool;
void enumerate_devices() {
IDeckLinkIterator* it = CreateDeckLinkIteratorInstance();
if (!it) throw std::runtime_error("DeckLink drivers not installed");
IDeckLink* device = nullptr;
uint32_t index = 0;
while (it->Next(&device) == S_OK) {
IDeckLinkInput* inp = nullptr;
if (device->QueryInterface(IID_IDeckLinkOutput, (void**)&inp) == S_OK) {
inp->Release();
const char* name = nullptr;
device->GetDisplayName(&name);
devices.push_back({index, name ? name : "?"});
raw_devices.push_back(device);
index++;
} else {
device->Release();
}
}
it->Release();
if (raw_devices.empty())
throw std::runtime_error("No DeckLink output devices found");
}
};
}