Files
dmf-studio-rnd/shared/DeckLinkSender.hpp
JohannesItten 32fe6aa22a fix: DeckLink macOS build — CFStringRef and CoreFoundation
GetDisplayName returns CFStringRef on macOS (not const char*).
Add #ifdef __APPLE__ handling in DeckLinkSender.hpp and
DeckLinkReceiver.hpp, and link -framework CoreFoundation in
decklinkin/decklinkout CMakeLists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 19:58:46 +03:00

296 lines
12 KiB
C++

#pragma once
#include <algorithm>
#include <atomic>
#include <cmath>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
#include <DeckLinkAPI.h>
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#endif
#include "Signal.hpp"
#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;
DeckLinkSender() { enumerate_devices(); }
~DeckLinkSender() {
if (decklink_output) {
if (playback_started_) decklink_output->StopScheduledPlayback(0, nullptr, 1);
decklink_output->DisableVideoOutput();
if (has_audio) decklink_output->DisableAudioOutput();
decklink_output->SetScheduledFrameCompletionCallback(nullptr);
decklink_output->Release();
}
delete output_callback;
for (auto* vf : all_frames) vf->Release();
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, height, fps_num, fps_den};
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");
const BMDDisplayMode mode = pick_display_mode(width, height, fps_num, fps_den);
// Verify the card supports this mode in 10-bit YUV before enabling
bool is_supported = false;
BMDDisplayMode actual_mode = mode;
r = decklink_output->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
mode,
bmdFormat10BitYUV,
bmdNoVideoOutputConversion,
bmdSupportedVideoModeDefault,
&actual_mode,
&is_supported);
if (r != S_OK || !is_supported)
throw std::runtime_error("Display mode not supported in 10-bit YUV");
r = decklink_output->EnableVideoOutput(actual_mode, 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");
audio_info.channels = channels;
has_audio = true;
}
int32_t row_bytes = 0;
r = decklink_output->RowBytesForPixelFormat(bmdFormat10BitYUV, width, &row_bytes);
if (r != S_OK) throw std::runtime_error("Could not get row bytes for pixel format");
// Create frame pool. Frames start in free_frames; submit_frame() schedules
// them as live content arrives. StartScheduledPlayback is deferred until
// kPreroll frames have been scheduled so the card has a buffer to draw from.
for (size_t i = 0; i < kPreroll; ++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 video frame");
all_frames.push_back(vf);
free_frames.push_back(vf);
}
}
// Blocks until a frame slot is free (returned by the card via callback),
// then copies src into it and schedules it for display.
void submit_frame(const uint8_t* src, uint32_t stride) {
IDeckLinkMutableVideoFrame* vf = nullptr;
{
std::unique_lock<std::mutex> lk(pool_mutex);
pool_cv.wait(lk, [this] {
return !free_frames.empty() ||
!dmf::g_running.load(std::memory_order_relaxed);
});
if (free_frames.empty()) return;
vf = free_frames.back();
free_frames.pop_back();
}
IDeckLinkVideoBuffer* buf = nullptr;
if (vf->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf) != S_OK) return;
buf->StartAccess(bmdBufferAccessWrite);
void* ptr = nullptr;
buf->GetBytes(&ptr);
if (ptr) {
const uint32_t dst_stride = static_cast<uint32_t>(vf->GetRowBytes());
const uint32_t copy_stride = 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_stride);
}
buf->EndAccess(bmdBufferAccessWrite);
buf->Release();
const int64_t t = scheduled_time;
scheduled_time += video_info.fps_den;
decklink_output->ScheduleVideoFrame(vf, t, video_info.fps_den, video_info.fps_num);
if (!playback_started_ && ++frames_scheduled_ >= kPreroll) {
playback_started_ = true;
decklink_output->StartScheduledPlayback(0, video_info.fps_num, 1.0);
}
}
// Converts float32 planar → interleaved int32 and pushes to DeckLink audio buffer.
void submit_audio(const float* planar, int samples) {
if (!has_audio || samples <= 0) return;
const int ch = audio_info.channels;
const size_t total = static_cast<size_t>(ch) * static_cast<size_t>(samples);
if (audio_convert_buf.size() < total) audio_convert_buf.resize(total);
auto& interleaved = audio_convert_buf;
for (int s = 0; s < samples; ++s)
for (int c = 0; c < ch; ++c) {
float v = std::max(-1.0f, std::min(1.0f, planar[c * samples + s]));
interleaved[static_cast<size_t>(s) * ch + c] =
static_cast<int32_t>(v * 2147483647.0f);
}
const int64_t t = audio_stream_time;
audio_stream_time += samples;
uint32_t written = 0;
decklink_output->ScheduleAudioSamples(
interleaved.data(), static_cast<uint32_t>(samples),
t, audio_info.sample_rate, &written);
}
private:
class OutputCallback : public IDeckLinkVideoOutputCallback {
public:
explicit OutputCallback(DeckLinkSender& owner) : owner(owner) {}
HRESULT ScheduledFrameCompleted(IDeckLinkVideoFrame* completed,
BMDOutputFrameCompletionResult) override {
{
std::lock_guard<std::mutex> lk(owner.pool_mutex);
owner.free_frames.push_back(
static_cast<IDeckLinkMutableVideoFrame*>(completed));
}
owner.pool_cv.notify_one();
return S_OK;
}
HRESULT ScheduledPlaybackHasStopped() 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; }
private:
DeckLinkSender& owner;
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*> all_frames; // for destructor cleanup
// Frame pool — frames returned by ScheduledFrameCompleted land here
std::mutex pool_mutex;
std::condition_variable pool_cv;
std::vector<IDeckLinkMutableVideoFrame*> free_frames;
static constexpr size_t kPreroll = 3;
// Scheduling state — only written from submit_frame/submit_audio (single-threaded caller)
bool playback_started_ = false;
size_t frames_scheduled_ = 0;
int64_t scheduled_time = 0; // next video frame position (fps_num units)
int64_t audio_stream_time = 0; // next audio batch position (sample units)
std::vector<int32_t> audio_convert_buf; // reused across submit_audio calls
// Maps width/height/fps to a BMDDisplayMode. Uses float comparison to handle
// any representation of drop-frame rates (e.g. 30000/1001 or 2997/100).
static BMDDisplayMode pick_display_mode(int width, int height, int fps_num, int fps_den) {
const double fps = static_cast<double>(fps_num) / static_cast<double>(fps_den);
if (width == 1920 && height == 1080) {
if (std::abs(fps - 23.976) < 0.01) return bmdModeHD1080p2398;
else if (std::abs(fps - 24.0) < 0.01) return bmdModeHD1080p24;
else if (std::abs(fps - 25.0) < 0.01) return bmdModeHD1080p25;
else if (std::abs(fps - 29.97) < 0.01) return bmdModeHD1080p2997;
else if (std::abs(fps - 30.0) < 0.01) return bmdModeHD1080p30;
else if (std::abs(fps - 50.0) < 0.01) return bmdModeHD1080p50;
else if (std::abs(fps - 59.94) < 0.01) return bmdModeHD1080p5994;
else if (std::abs(fps - 60.0) < 0.01) return bmdModeHD1080p6000;
} else if (width == 1280 && height == 720) {
if (std::abs(fps - 50.0) < 0.01) return bmdModeHD720p50;
else if (std::abs(fps - 59.94) < 0.01) return bmdModeHD720p5994;
else if (std::abs(fps - 60.0) < 0.01) return bmdModeHD720p60;
}
throw std::runtime_error(
"No DeckLink display mode for " + std::to_string(width) + "x" +
std::to_string(height) + " @ " + std::to_string(fps_num) +
"/" + std::to_string(fps_den) + " fps");
}
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) {
IDeckLinkOutput* out = nullptr;
if (device->QueryInterface(IID_IDeckLinkOutput, (void**)&out) == S_OK) {
out->Release();
std::string name = "?";
#ifdef __APPLE__
CFStringRef cfName = nullptr;
if (device->GetDisplayName(&cfName) == S_OK && cfName) {
char buf[256] = {};
CFStringGetCString(cfName, buf, sizeof(buf), kCFStringEncodingUTF8);
name = buf;
CFRelease(cfName);
}
#else
const char* cname = nullptr;
if (device->GetDisplayName(&cname) == S_OK && cname) name = cname;
#endif
devices.push_back({index, 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");
}
};
} // namespace dmf