NDI output
This commit is contained in:
253
src/video/ndi/NdiOutput.cpp
Normal file
253
src/video/ndi/NdiOutput.cpp
Normal file
@@ -0,0 +1,253 @@
|
||||
#include "NdiOutput.h"
|
||||
|
||||
#include "NdiRuntime.h"
|
||||
#include "logging/Logger.h"
|
||||
#include "video/core/VideoIOFormat.h"
|
||||
|
||||
#include <Processing.NDI.Lib.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string ResolveSenderName(const std::string& configuredName)
|
||||
{
|
||||
if (configuredName.empty() || configuredName == "default" || configuredName == "auto")
|
||||
return "Render Cadence";
|
||||
return configuredName;
|
||||
}
|
||||
|
||||
void SetCommonVideoFrameFields(NDIlib_video_frame_v2_t& ndiFrame, const VideoIOOutputFrame& frame, const VideoOutputEdgeConfig& config)
|
||||
{
|
||||
std::memset(&ndiFrame, 0, sizeof(ndiFrame));
|
||||
ndiFrame.xres = static_cast<int>(frame.width);
|
||||
ndiFrame.yres = static_cast<int>(frame.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 = frame.height > 0 ? static_cast<float>(frame.width) / static_cast<float>(frame.height) : 0.0f;
|
||||
ndiFrame.frame_format_type = NDIlib_frame_format_type_progressive;
|
||||
ndiFrame.timecode = NDIlib_send_timecode_synthesize;
|
||||
ndiFrame.p_data = static_cast<uint8_t*>(frame.bytes);
|
||||
ndiFrame.line_stride_in_bytes = static_cast<int>(frame.rowBytes);
|
||||
}
|
||||
}
|
||||
|
||||
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 (!mRunning.load(std::memory_order_acquire) || frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8)
|
||||
{
|
||||
++mScheduleFailures;
|
||||
return false;
|
||||
}
|
||||
|
||||
NDIlib_video_frame_v2_t ndiFrame;
|
||||
SetCommonVideoFrameFields(ndiFrame, frame, mConfig);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mSender == nullptr)
|
||||
{
|
||||
++mScheduleFailures;
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(mSender), &ndiFrame);
|
||||
mScheduleCallMilliseconds.store(
|
||||
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(std::chrono::steady_clock::now() - scheduleStart).count(),
|
||||
std::memory_order_relaxed);
|
||||
|
||||
CompletePendingLocked(VideoIOCompletionResult::Completed);
|
||||
mPendingBuffer = frame.nativeBuffer != nullptr ? frame.nativeBuffer : frame.bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
void NdiOutput::Stop()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mSender != nullptr)
|
||||
FlushPendingLocked();
|
||||
mRunning.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void NdiOutput::ReleaseResources()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mSender != nullptr)
|
||||
{
|
||||
FlushPendingLocked();
|
||||
NDIlib_send_destroy(static_cast<NDIlib_send_instance_t>(mSender));
|
||||
mSender = 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<std::mutex> lock(mMutex);
|
||||
metrics.actualBufferedFrames = mPendingBuffer != nullptr ? 1 : 0;
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
void NdiOutput::CompletePendingLocked(VideoIOCompletionResult result)
|
||||
{
|
||||
if (mPendingBuffer == nullptr)
|
||||
return;
|
||||
|
||||
VideoIOCompletion completion;
|
||||
completion.result = result;
|
||||
completion.outputFrameBuffer = mPendingBuffer;
|
||||
mPendingBuffer = nullptr;
|
||||
++mCompletions;
|
||||
if (result == VideoIOCompletionResult::Dropped)
|
||||
++mDropped;
|
||||
else if (result == VideoIOCompletionResult::Flushed)
|
||||
++mFlushed;
|
||||
|
||||
if (mCompletionCallback)
|
||||
mCompletionCallback(completion);
|
||||
}
|
||||
|
||||
void NdiOutput::FlushPendingLocked()
|
||||
{
|
||||
if (mSender != nullptr)
|
||||
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(mSender), nullptr);
|
||||
CompletePendingLocked(VideoIOCompletionResult::Flushed);
|
||||
}
|
||||
|
||||
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.";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user