fixes + refactoring + plan
This commit is contained in:
+135
-97
@@ -1,11 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <DeckLinkAPI.h>
|
||||
|
||||
#include "Signal.hpp"
|
||||
#include "V210.hpp"
|
||||
|
||||
namespace dmf {
|
||||
@@ -33,38 +39,30 @@ public:
|
||||
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->StopScheduledPlayback(0, nullptr, 1);
|
||||
decklink_output->DisableVideoOutput();
|
||||
if (has_audio) decklink_output->DisableAudioOutput();;
|
||||
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* 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) {
|
||||
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
|
||||
video_info = {width, height, fps_num, fps_den};
|
||||
|
||||
selected_device = raw_devices[device_index];
|
||||
selected_device->AddRef();
|
||||
@@ -76,7 +74,23 @@ public:
|
||||
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);
|
||||
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) {
|
||||
@@ -85,36 +99,24 @@ public:
|
||||
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;
|
||||
has_audio = true;
|
||||
}
|
||||
|
||||
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;
|
||||
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 display mode");
|
||||
if (r != S_OK) throw std::runtime_error("Could not get row bytes for pixel format");
|
||||
|
||||
// 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) {
|
||||
// Pre-fill 3 black frames and schedule them to prime the pipeline.
|
||||
// The card fires ScheduledFrameCompleted once each is done, at which point
|
||||
// submit_frame() can reclaim and reschedule them with live video.
|
||||
constexpr size_t kPreroll = 3;
|
||||
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 a video frame");
|
||||
r = decklink_output->CreateVideoFrame(
|
||||
width, height, row_bytes, bmdFormat10BitYUV, bmdFrameFlagDefault, &vf);
|
||||
if (r != S_OK) throw std::runtime_error("Could not create video frame");
|
||||
|
||||
IDeckLinkVideoBuffer* buf = nullptr;
|
||||
vf->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&buf);
|
||||
buf->StartAccess(bmdBufferAccessWrite);
|
||||
@@ -124,20 +126,30 @@ public:
|
||||
buf->EndAccess(bmdBufferAccessWrite);
|
||||
buf->Release();
|
||||
|
||||
decklink_output->ScheduleVideoFrame(vf, i * duration, duration, timescale);
|
||||
frame_pool.push_back(vf);
|
||||
decklink_output->ScheduleVideoFrame(
|
||||
vf, static_cast<BMDTimeValue>(i * fps_den), fps_den, fps_num);
|
||||
all_frames.push_back(vf);
|
||||
}
|
||||
output_callback->next_time.store(preroll_pool_size * duration);
|
||||
scheduled_time = static_cast<int64_t>(kPreroll) * fps_den;
|
||||
|
||||
// start playback
|
||||
r = decklink_output->StartScheduledPlayback(0, timescale, 1.0);
|
||||
if (r != S_OK) throw std::runtime_error("Could not start streams");
|
||||
r = decklink_output->StartScheduledPlayback(0, fps_num, 1.0);
|
||||
if (r != S_OK) throw std::runtime_error("Could not start scheduled playback");
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Grab a free frame from the pool (round-robin)
|
||||
IDeckLinkMutableVideoFrame* vf = frame_pool[frame_count % frame_pool.size()];
|
||||
frame_count++;
|
||||
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;
|
||||
@@ -145,83 +157,108 @@ public:
|
||||
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) {
|
||||
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_row);
|
||||
}
|
||||
src + y * stride, copy_stride);
|
||||
}
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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] =
|
||||
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 stream_time = audio_stream_time.fetch_add(samples);
|
||||
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),
|
||||
stream_time, audio_info.sample_rate, &written);
|
||||
(void)written;
|
||||
t, audio_info.sample_rate, &written);
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
class OutputCallback: public IDeckLinkVideoOutputCallback {
|
||||
public:
|
||||
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.
|
||||
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 (void) override {
|
||||
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; }
|
||||
|
||||
std::atomic<int64_t> next_time{0};
|
||||
|
||||
private:
|
||||
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<IDeckLink*> raw_devices;
|
||||
IDeckLink* selected_device = nullptr;
|
||||
IDeckLinkOutput* decklink_output = nullptr;
|
||||
OutputCallback* output_callback = nullptr;
|
||||
std::vector<IDeckLinkMutableVideoFrame*> all_frames; // for destructor cleanup
|
||||
|
||||
std::vector<IDeckLinkMutableVideoFrame*> frame_pool;
|
||||
// Frame pool — frames returned by ScheduledFrameCompleted land here
|
||||
std::mutex pool_mutex;
|
||||
std::condition_variable pool_cv;
|
||||
std::vector<IDeckLinkMutableVideoFrame*> free_frames;
|
||||
|
||||
// Scheduling state — only written from submit_frame/submit_audio (single-threaded caller)
|
||||
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();
|
||||
@@ -230,9 +267,9 @@ private:
|
||||
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();
|
||||
IDeckLinkOutput* out = nullptr;
|
||||
if (device->QueryInterface(IID_IDeckLinkOutput, (void**)&out) == S_OK) {
|
||||
out->Release();
|
||||
const char* name = nullptr;
|
||||
device->GetDisplayName(&name);
|
||||
devices.push_back({index, name ? name : "?"});
|
||||
@@ -247,4 +284,5 @@ private:
|
||||
throw std::runtime_error("No DeckLink output devices found");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dmf
|
||||
|
||||
Reference in New Issue
Block a user