#pragma once #include #include #include #include #include #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; int64_t frame_count = 0; std::atomic 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(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(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(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 interleaved( static_cast(channels) * static_cast(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(s) * channels + c] = static_cast(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(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 next_time{0}; private: DeckLinkSender& owner; std::atomic next_frame_idx{0}; 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 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"); } }; }