From 80c6fd2434dc4fb9508dc354932d36c5a5cf5c2a Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 22 May 2026 16:51:48 +1000 Subject: [PATCH] NDI cleanup to avoid calling NDI while holding the output mutex --- src/video/ndi/NdiOutput.cpp | 93 ++++++++++++++++++++++++------------- src/video/ndi/NdiOutput.h | 4 +- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/src/video/ndi/NdiOutput.cpp b/src/video/ndi/NdiOutput.cpp index 386da63..dc4910e 100644 --- a/src/video/ndi/NdiOutput.cpp +++ b/src/video/ndi/NdiOutput.cpp @@ -134,7 +134,7 @@ bool NdiOutput::StartScheduledPlayback(std::string& error) bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame) { - if (!mRunning.load(std::memory_order_acquire) || frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8) + if (frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8) { ++mScheduleFailures; return false; @@ -143,44 +143,79 @@ bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame) NDIlib_video_frame_v2_t ndiFrame; SetCommonVideoFrameFields(ndiFrame, frame, mConfig); - std::lock_guard lock(mMutex); - if (mSender == nullptr) + void* completedBuffer = nullptr; { - ++mScheduleFailures; - return false; + 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; + } + + 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; } - const auto scheduleStart = std::chrono::steady_clock::now(); - NDIlib_send_send_video_async_v2(static_cast(mSender), &ndiFrame); - mScheduleCallMilliseconds.store( - std::chrono::duration_cast>(std::chrono::steady_clock::now() - scheduleStart).count(), - std::memory_order_relaxed); - - CompletePendingLocked(VideoIOCompletionResult::Completed); - mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes; + CompleteBuffer(completedBuffer, VideoIOCompletionResult::Completed); return true; } void NdiOutput::Stop() { - std::lock_guard lock(mMutex); - if (mSender != nullptr) - FlushPendingLocked(); - mRunning.store(false, std::memory_order_release); + 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); + } + + CompleteBuffer(flushedBuffer, VideoIOCompletionResult::Flushed); } void NdiOutput::ReleaseResources() { + void* sender = nullptr; + void* flushedBuffer = nullptr; { - std::lock_guard lock(mMutex); - if (mSender != nullptr) + std::lock_guard sdkLock(mSdkMutex); { - FlushPendingLocked(); - NDIlib_send_destroy(static_cast(mSender)); + std::lock_guard stateLock(mMutex); + sender = mSender; mSender = nullptr; - ReleaseNdiRuntime(); + flushedBuffer = mPendingBuffer; + mPendingBuffer = nullptr; + } + 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); } @@ -201,15 +236,14 @@ NdiOutputMetrics NdiOutput::Metrics() const return metrics; } -void NdiOutput::CompletePendingLocked(VideoIOCompletionResult result) +void NdiOutput::CompleteBuffer(void* buffer, VideoIOCompletionResult result) { - if (mPendingBuffer == nullptr) + if (buffer == nullptr) return; VideoIOCompletion completion; completion.result = result; - completion.outputFrameBuffer = mPendingBuffer; - mPendingBuffer = nullptr; + completion.outputFrameBuffer = buffer; ++mCompletions; if (result == VideoIOCompletionResult::Dropped) ++mDropped; @@ -220,13 +254,6 @@ void NdiOutput::CompletePendingLocked(VideoIOCompletionResult result) mCompletionCallback(completion); } -void NdiOutput::FlushPendingLocked() -{ - if (mSender != nullptr) - NDIlib_send_send_video_async_v2(static_cast(mSender), nullptr); - CompletePendingLocked(VideoIOCompletionResult::Flushed); -} - void NdiOutput::PopulateState(VideoIOState& state, const VideoOutputEdgeConfig& config, const std::string& senderName) { state = VideoIOState(); diff --git a/src/video/ndi/NdiOutput.h b/src/video/ndi/NdiOutput.h index 8baddb2..fada968 100644 --- a/src/video/ndi/NdiOutput.h +++ b/src/video/ndi/NdiOutput.h @@ -30,13 +30,13 @@ public: NdiOutputMetrics Metrics() const override; private: - void CompletePendingLocked(VideoIOCompletionResult result); - void FlushPendingLocked(); + void CompleteBuffer(void* buffer, VideoIOCompletionResult result); static void PopulateState(VideoIOState& state, const VideoOutputEdgeConfig& config, const std::string& senderName); std::string mSenderName; CompletionCallback mCompletionCallback; mutable std::mutex mMutex; + mutable std::mutex mSdkMutex; void* mSender = nullptr; void* mPendingBuffer = nullptr; VideoOutputEdgeConfig mConfig;