NDI output flip
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m47s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-22 17:04:27 +10:00
parent 80c6fd2434
commit c5f0a9df0e
2 changed files with 64 additions and 13 deletions

View File

@@ -8,6 +8,7 @@
#include <chrono> #include <chrono>
#include <cstring> #include <cstring>
#include <vector>
#include <utility> #include <utility>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
@@ -21,11 +22,19 @@ std::string ResolveSenderName(const std::string& configuredName)
return 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)); std::memset(&ndiFrame, 0, sizeof(ndiFrame));
ndiFrame.xres = static_cast<int>(frame.width); ndiFrame.xres = static_cast<int>(width);
ndiFrame.yres = static_cast<int>(frame.height); ndiFrame.yres = static_cast<int>(height);
ndiFrame.FourCC = NDIlib_FourCC_video_type_BGRA; ndiFrame.FourCC = NDIlib_FourCC_video_type_BGRA;
ndiFrame.frame_rate_N = 60000; ndiFrame.frame_rate_N = 60000;
ndiFrame.frame_rate_D = 1001; 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_N = 24000;
ndiFrame.frame_rate_D = 1001; ndiFrame.frame_rate_D = 1001;
} }
ndiFrame.picture_aspect_ratio = frame.height > 0 ? static_cast<float>(frame.width) / static_cast<float>(frame.height) : 0.0f; ndiFrame.picture_aspect_ratio = height > 0 ? static_cast<float>(width) / static_cast<float>(height) : 0.0f;
ndiFrame.frame_format_type = NDIlib_frame_format_type_progressive; ndiFrame.frame_format_type = NDIlib_frame_format_type_progressive;
ndiFrame.timecode = NDIlib_send_timecode_synthesize; ndiFrame.timecode = NDIlib_send_timecode_synthesize;
ndiFrame.p_data = static_cast<uint8_t*>(frame.bytes); ndiFrame.p_data = static_cast<uint8_t*>(pixels);
ndiFrame.line_stride_in_bytes = static_cast<int>(frame.rowBytes); ndiFrame.line_stride_in_bytes = rowBytes;
}
bool CopyBgra8FrameFlippedVertically(const VideoIOOutputFrame& frame, std::vector<unsigned char>& 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<std::size_t>(frame.width);
const std::size_t height = static_cast<std::size_t>(frame.height);
const std::size_t sourceRowBytes = static_cast<std::size_t>(frame.rowBytes);
const std::size_t destinationRowBytes = width * kBgraBytesPerPixel;
if (sourceRowBytes < destinationRowBytes)
return false;
outputPixels.resize(destinationRowBytes * height);
const unsigned char* sourceBytes = static_cast<const unsigned char*>(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<int>(destinationRowBytes);
return true;
} }
} }
@@ -140,9 +174,6 @@ bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
return false; return false;
} }
NDIlib_video_frame_v2_t ndiFrame;
SetCommonVideoFrameFields(ndiFrame, frame, mConfig);
void* completedBuffer = nullptr; void* completedBuffer = nullptr;
{ {
std::lock_guard<std::mutex> sdkLock(mSdkMutex); std::lock_guard<std::mutex> sdkLock(mSdkMutex);
@@ -157,15 +188,30 @@ bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
sender = mSender; sender = mSender;
} }
std::vector<unsigned char>& 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(); const auto scheduleStart = std::chrono::steady_clock::now();
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(sender), &ndiFrame); NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(sender), &ndiFrame);
mScheduleCallMilliseconds.store( mScheduleCallMilliseconds.store(
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(std::chrono::steady_clock::now() - scheduleStart).count(), std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(std::chrono::steady_clock::now() - scheduleStart).count(),
std::memory_order_relaxed); std::memory_order_relaxed);
std::lock_guard<std::mutex> stateLock(mMutex); mNextStagingBuffer = (mNextStagingBuffer + 1) % 2;
completedBuffer = mPendingBuffer; mHasInFlightFrame.store(true, std::memory_order_release);
mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes; {
std::lock_guard<std::mutex> stateLock(mMutex);
completedBuffer = mPendingBuffer;
mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes;
}
} }
CompleteBuffer(completedBuffer, VideoIOCompletionResult::Completed); CompleteBuffer(completedBuffer, VideoIOCompletionResult::Completed);
@@ -189,8 +235,8 @@ void NdiOutput::Stop()
flushedBuffer = mPendingBuffer; flushedBuffer = mPendingBuffer;
mPendingBuffer = nullptr; mPendingBuffer = nullptr;
mRunning.store(false, std::memory_order_release); mRunning.store(false, std::memory_order_release);
mHasInFlightFrame.store(false, std::memory_order_release);
} }
CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed); CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed);
} }
@@ -206,6 +252,7 @@ void NdiOutput::ReleaseResources()
mSender = nullptr; mSender = nullptr;
flushedBuffer = mPendingBuffer; flushedBuffer = mPendingBuffer;
mPendingBuffer = nullptr; mPendingBuffer = nullptr;
mHasInFlightFrame.store(false, std::memory_order_release);
} }
if (sender != nullptr) if (sender != nullptr)
{ {

View File

@@ -6,6 +6,7 @@
#include <cstdint> #include <cstdint>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <vector>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
@@ -39,10 +40,13 @@ private:
mutable std::mutex mSdkMutex; mutable std::mutex mSdkMutex;
void* mSender = nullptr; void* mSender = nullptr;
void* mPendingBuffer = nullptr; void* mPendingBuffer = nullptr;
std::vector<unsigned char> mStagingBuffers[2];
std::size_t mNextStagingBuffer = 0;
VideoOutputEdgeConfig mConfig; VideoOutputEdgeConfig mConfig;
VideoIOState mState; VideoIOState mState;
std::atomic<bool> mInitialized{ false }; std::atomic<bool> mInitialized{ false };
std::atomic<bool> mRunning{ false }; std::atomic<bool> mRunning{ false };
std::atomic<bool> mHasInFlightFrame{ false };
std::atomic<uint64_t> mCompletions{ 0 }; std::atomic<uint64_t> mCompletions{ 0 };
std::atomic<uint64_t> mDropped{ 0 }; std::atomic<uint64_t> mDropped{ 0 };
std::atomic<uint64_t> mFlushed{ 0 }; std::atomic<uint64_t> mFlushed{ 0 };