#include "NdiOutput.h" #include "NdiRuntime.h" #include "logging/Logger.h" #include "video/core/VideoIOFormat.h" #include #include #include #include #include namespace RenderCadenceCompositor { namespace { std::string ResolveSenderName(const std::string& configuredName) { if (configuredName.empty() || configuredName == "default" || configuredName == "auto") return "Render Cadence"; return configuredName; } constexpr std::size_t kBgraBytesPerPixel = 4; void SetCommonVideoFrameFields( NDIlib_video_frame_v2_t& ndiFrame, unsigned int width, unsigned int height, int rowBytes, void* pixels, const VideoOutputEdgeConfig& config) { std::memset(&ndiFrame, 0, sizeof(ndiFrame)); ndiFrame.xres = static_cast(width); ndiFrame.yres = static_cast(height); ndiFrame.FourCC = NDIlib_FourCC_video_type_BGRA; ndiFrame.frame_rate_N = 60000; ndiFrame.frame_rate_D = 1001; if (config.outputVideoMode.frameRate == "60") { ndiFrame.frame_rate_N = 60; ndiFrame.frame_rate_D = 1; } else if (config.outputVideoMode.frameRate == "50") { ndiFrame.frame_rate_N = 50; ndiFrame.frame_rate_D = 1; } else if (config.outputVideoMode.frameRate == "30") { ndiFrame.frame_rate_N = 30; ndiFrame.frame_rate_D = 1; } else if (config.outputVideoMode.frameRate == "29.97") { ndiFrame.frame_rate_N = 30000; ndiFrame.frame_rate_D = 1001; } else if (config.outputVideoMode.frameRate == "25") { ndiFrame.frame_rate_N = 25; ndiFrame.frame_rate_D = 1; } else if (config.outputVideoMode.frameRate == "24") { ndiFrame.frame_rate_N = 24; ndiFrame.frame_rate_D = 1; } else if (config.outputVideoMode.frameRate == "23.98") { ndiFrame.frame_rate_N = 24000; ndiFrame.frame_rate_D = 1001; } ndiFrame.picture_aspect_ratio = height > 0 ? static_cast(width) / static_cast(height) : 0.0f; ndiFrame.frame_format_type = NDIlib_frame_format_type_progressive; ndiFrame.timecode = NDIlib_send_timecode_synthesize; ndiFrame.p_data = static_cast(pixels); ndiFrame.line_stride_in_bytes = rowBytes; } bool CopyBgra8FrameFlippedVertically(const VideoIOOutputFrame& frame, std::vector& outputPixels, int& outputRowBytes) { if (frame.bytes == nullptr || frame.width == 0 || frame.height == 0 || frame.rowBytes <= 0) return false; const std::size_t width = static_cast(frame.width); const std::size_t height = static_cast(frame.height); const std::size_t sourceRowBytes = static_cast(frame.rowBytes); const std::size_t destinationRowBytes = width * kBgraBytesPerPixel; if (sourceRowBytes < destinationRowBytes) return false; outputPixels.resize(destinationRowBytes * height); const unsigned char* sourceBytes = static_cast(frame.bytes); for (std::size_t y = 0; y < height; ++y) { const unsigned char* sourceRow = sourceBytes + (height - 1u - y) * sourceRowBytes; unsigned char* destinationRow = outputPixels.data() + y * destinationRowBytes; std::memcpy(destinationRow, sourceRow, destinationRowBytes); } outputRowBytes = static_cast(destinationRowBytes); return true; } } NdiOutput::NdiOutput(std::string senderName) : mSenderName(ResolveSenderName(senderName)) { } NdiOutput::~NdiOutput() { ReleaseResources(); } bool NdiOutput::Initialize(const VideoOutputEdgeConfig& config, CompletionCallback completionCallback, std::string& error) { ReleaseResources(); mConfig = config; mCompletionCallback = std::move(completionCallback); PopulateState(mState, config, mSenderName); if (config.outputAlphaRequired) { mState.statusMessage = "NDI output sends BGRA frames with alpha when present; output.keying.alphaRequired is a DeckLink-only requirement."; } if (!AcquireNdiRuntime()) { error = "NDI runtime initialization failed. The CPU/runtime might not support NDI."; return false; } NDIlib_send_create_t createSettings; std::memset(&createSettings, 0, sizeof(createSettings)); createSettings.p_ndi_name = mSenderName.c_str(); createSettings.clock_video = false; createSettings.clock_audio = false; mSender = NDIlib_send_create(&createSettings); if (mSender == nullptr) { ReleaseNdiRuntime(); error = "NDI sender creation failed."; return false; } mInitialized.store(true, std::memory_order_release); mState.statusMessage = "NDI output sender '" + mSenderName + "' initialized."; Log("ndi-output", mState.statusMessage); return true; } bool NdiOutput::StartScheduledPlayback(std::string& error) { if (!mInitialized.load(std::memory_order_acquire) || mSender == nullptr) { error = "NDI output has not been initialized."; return false; } mRunning.store(true, std::memory_order_release); Log("ndi-output", "NDI output sender started."); return true; } bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame) { if (frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8) { ++mScheduleFailures; return false; } void* completedBuffer = nullptr; { std::lock_guard sdkLock(mSdkMutex); void* sender = nullptr; { std::lock_guard stateLock(mMutex); if (!mRunning.load(std::memory_order_acquire) || mSender == nullptr) { ++mScheduleFailures; return false; } sender = mSender; } std::vector& stagingBuffer = mStagingBuffers[mNextStagingBuffer]; int stagingRowBytes = 0; if (!CopyBgra8FrameFlippedVertically(frame, stagingBuffer, stagingRowBytes)) { ++mScheduleFailures; return false; } NDIlib_video_frame_v2_t ndiFrame; SetCommonVideoFrameFields(ndiFrame, frame.width, frame.height, stagingRowBytes, stagingBuffer.data(), mConfig); const auto scheduleStart = std::chrono::steady_clock::now(); NDIlib_send_send_video_async_v2(static_cast(sender), &ndiFrame); mScheduleCallMilliseconds.store( std::chrono::duration_cast>(std::chrono::steady_clock::now() - scheduleStart).count(), std::memory_order_relaxed); mNextStagingBuffer = (mNextStagingBuffer + 1) % 2; mHasInFlightFrame.store(true, std::memory_order_release); { std::lock_guard stateLock(mMutex); completedBuffer = mPendingBuffer; mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes; } } CompleteBuffer(completedBuffer, VideoIOCompletionResult::Completed); return true; } void NdiOutput::Stop() { void* flushedBuffer = nullptr; { std::lock_guard sdkLock(mSdkMutex); void* sender = nullptr; { std::lock_guard stateLock(mMutex); sender = mSender; } if (sender != nullptr) NDIlib_send_send_video_async_v2(static_cast(sender), nullptr); std::lock_guard stateLock(mMutex); flushedBuffer = mPendingBuffer; mPendingBuffer = nullptr; mRunning.store(false, std::memory_order_release); mHasInFlightFrame.store(false, std::memory_order_release); } CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed); } void NdiOutput::ReleaseResources() { void* sender = nullptr; void* flushedBuffer = nullptr; { std::lock_guard sdkLock(mSdkMutex); { std::lock_guard stateLock(mMutex); sender = mSender; mSender = nullptr; flushedBuffer = mPendingBuffer; mPendingBuffer = nullptr; mHasInFlightFrame.store(false, std::memory_order_release); } if (sender != nullptr) { NDIlib_send_send_video_async_v2(static_cast(sender), nullptr); NDIlib_send_destroy(static_cast(sender)); } } CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed); if (sender != nullptr) ReleaseNdiRuntime(); mInitialized.store(false, std::memory_order_release); mRunning.store(false, std::memory_order_release); } NdiOutputMetrics NdiOutput::Metrics() const { NdiOutputMetrics metrics; metrics.completions = mCompletions.load(std::memory_order_relaxed); metrics.dropped = mDropped.load(std::memory_order_relaxed); metrics.flushed = mFlushed.load(std::memory_order_relaxed); metrics.scheduleFailures = mScheduleFailures.load(std::memory_order_relaxed); metrics.scheduleCallMilliseconds = mScheduleCallMilliseconds.load(std::memory_order_relaxed); metrics.actualBufferedFramesAvailable = false; { std::lock_guard lock(mMutex); metrics.actualBufferedFrames = mPendingBuffer != nullptr ? 1 : 0; } return metrics; } void NdiOutput::CompleteBuffer(void* buffer, VideoIOCompletionResult result) { if (buffer == nullptr) return; VideoIOCompletion completion; completion.result = result; completion.outputFrameBuffer = buffer; ++mCompletions; if (result == VideoIOCompletionResult::Dropped) ++mDropped; else if (result == VideoIOCompletionResult::Flushed) ++mFlushed; if (mCompletionCallback) mCompletionCallback(completion); } void NdiOutput::PopulateState(VideoIOState& state, const VideoOutputEdgeConfig& config, const std::string& senderName) { state = VideoIOState(); state.outputFrameSize = config.outputVideoMode.frameSize; state.inputFrameSize = state.outputFrameSize; state.outputPixelFormat = VideoIOPixelFormat::Bgra8; state.inputPixelFormat = VideoIOPixelFormat::Bgra8; state.outputFrameRowBytes = VideoIORowBytes(state.outputPixelFormat, state.outputFrameSize.width); state.inputFrameRowBytes = state.outputFrameRowBytes; state.outputPackTextureWidth = state.outputFrameSize.width; state.captureTextureWidth = state.inputFrameSize.width; state.outputDisplayModeName = config.outputVideoMode.displayName; state.inputDisplayModeName = "No input - NDI output session"; state.outputModelName = "NDI: " + senderName; state.hasInputDevice = false; state.hasInputSource = false; state.supportsExternalKeying = false; state.supportsInternalKeying = false; state.keyerInterfaceAvailable = false; state.externalKeyingActive = false; state.frameBudgetMilliseconds = config.outputVideoMode.frameDurationMilliseconds; state.formatStatusMessage = "NDI output format: BGRA8."; } }