From c5f0a9df0ed821e128048885f3fb09795abe4be8 Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 22 May 2026 17:04:27 +1000 Subject: [PATCH] NDI output flip --- src/video/ndi/NdiOutput.cpp | 73 ++++++++++++++++++++++++++++++------- src/video/ndi/NdiOutput.h | 4 ++ 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/video/ndi/NdiOutput.cpp b/src/video/ndi/NdiOutput.cpp index dc4910e..c07415d 100644 --- a/src/video/ndi/NdiOutput.cpp +++ b/src/video/ndi/NdiOutput.cpp @@ -8,6 +8,7 @@ #include #include +#include #include namespace RenderCadenceCompositor @@ -21,11 +22,19 @@ std::string ResolveSenderName(const std::string& configuredName) return configuredName; } -void SetCommonVideoFrameFields(NDIlib_video_frame_v2_t& ndiFrame, const VideoIOOutputFrame& frame, const VideoOutputEdgeConfig& config) +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(frame.width); - ndiFrame.yres = static_cast(frame.height); + 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; @@ -64,11 +73,36 @@ void SetCommonVideoFrameFields(NDIlib_video_frame_v2_t& ndiFrame, const VideoIOO ndiFrame.frame_rate_N = 24000; ndiFrame.frame_rate_D = 1001; } - ndiFrame.picture_aspect_ratio = frame.height > 0 ? static_cast(frame.width) / static_cast(frame.height) : 0.0f; + 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(frame.bytes); - ndiFrame.line_stride_in_bytes = static_cast(frame.rowBytes); + 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; } } @@ -140,9 +174,6 @@ bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame) return false; } - NDIlib_video_frame_v2_t ndiFrame; - SetCommonVideoFrameFields(ndiFrame, frame, mConfig); - void* completedBuffer = nullptr; { std::lock_guard sdkLock(mSdkMutex); @@ -157,15 +188,30 @@ bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame) 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); - std::lock_guard stateLock(mMutex); - completedBuffer = mPendingBuffer; - mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes; + 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); @@ -189,8 +235,8 @@ void NdiOutput::Stop() flushedBuffer = mPendingBuffer; mPendingBuffer = nullptr; mRunning.store(false, std::memory_order_release); + mHasInFlightFrame.store(false, std::memory_order_release); } - CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed); } @@ -206,6 +252,7 @@ void NdiOutput::ReleaseResources() mSender = nullptr; flushedBuffer = mPendingBuffer; mPendingBuffer = nullptr; + mHasInFlightFrame.store(false, std::memory_order_release); } if (sender != nullptr) { diff --git a/src/video/ndi/NdiOutput.h b/src/video/ndi/NdiOutput.h index fada968..feb3f48 100644 --- a/src/video/ndi/NdiOutput.h +++ b/src/video/ndi/NdiOutput.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace RenderCadenceCompositor { @@ -39,10 +40,13 @@ private: mutable std::mutex mSdkMutex; void* mSender = nullptr; void* mPendingBuffer = nullptr; + std::vector mStagingBuffers[2]; + std::size_t mNextStagingBuffer = 0; VideoOutputEdgeConfig mConfig; VideoIOState mState; std::atomic mInitialized{ false }; std::atomic mRunning{ false }; + std::atomic mHasInFlightFrame{ false }; std::atomic mCompletions{ 0 }; std::atomic mDropped{ 0 }; std::atomic mFlushed{ 0 };