#pragma once #include #include #include #include #include #include #include #include #include #include #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 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(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 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(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(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(ch) * static_cast(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(s) * ch + c] = static_cast(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(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 lk(owner.pool_mutex); owner.free_frames.push_back( static_cast(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 ref_count{1}; }; // DeckLink SDK objects std::vector raw_devices; IDeckLink* selected_device = nullptr; IDeckLinkOutput* decklink_output = nullptr; OutputCallback* output_callback = nullptr; std::vector all_frames; // for destructor cleanup // Frame pool — frames returned by ScheduledFrameCompleted land here std::mutex pool_mutex; std::condition_variable pool_cv; std::vector 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 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(fps_num) / static_cast(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(); 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"); } }; } // namespace dmf