UYVY backend
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-30 19:16:16 +10:00
parent d0b1f63524
commit f0f8b080ca
30 changed files with 733 additions and 239 deletions

View File

@@ -55,6 +55,7 @@ struct VideoOutputEdgeMetrics
struct VideoOutputEdgeConfig
{
VideoFormat outputVideoMode;
VideoIOPixelFormat systemFramePixelFormat = VideoIOPixelFormat::Bgra8;
bool externalKeyingEnabled = false;
bool outputAlphaRequired = false;
};

View File

@@ -18,7 +18,7 @@ bool DeckLinkOutput::Initialize(const VideoOutputEdgeConfig& config, CompletionC
formats.output = config.outputVideoMode;
if (!mSession.DiscoverDevicesAndModes(formats, error))
return false;
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
if (!mSession.SelectPreferredFormats(formats, config.systemFramePixelFormat, config.outputAlphaRequired, error))
return false;
if (!mSession.ConfigureOutput(
[this](const VideoIOCompletion& completion) { HandleCompletion(completion); },

View File

@@ -117,6 +117,11 @@ bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, B
&supported);
return result == S_OK && supported != FALSE;
}
bool RenderReadbackSupportsOutputFormat(VideoIOPixelFormat pixelFormat)
{
return pixelFormat == VideoIOPixelFormat::Bgra8 || pixelFormat == VideoIOPixelFormat::Uyvy8;
}
}
DeckLinkSession::~DeckLinkSession()
@@ -254,7 +259,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
return true;
}
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, VideoIOPixelFormat systemFramePixelFormat, bool outputAlphaRequired, std::string& error)
{
if (!output)
{
@@ -271,18 +276,30 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
}
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
if (!RenderReadbackSupportsOutputFormat(systemFramePixelFormat))
{
error = "DeckLink output requested " + std::string(VideoIOPixelFormatName(systemFramePixelFormat)) +
", but render readback currently supports only BGRA8 and UYVY8 system frames.";
return false;
}
if (outputAlphaRequired && systemFramePixelFormat != VideoIOPixelFormat::Bgra8)
{
error = "DeckLink alpha output requires BGRA8 system frames until a YUVA render packer exists.";
return false;
}
const bool outputTenBitSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUV);
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, outputDisplayMode, bmdFormat10BitYUVA);
mState.outputPixelFormat = outputAlphaRequired
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
if (outputAlphaRequired && outputTenBitYuvaSupported)
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
else if (outputAlphaRequired)
mState.formatStatusMessage += "External keying requires alpha, but DeckLink output does not report 10-bit YUVA support for the configured mode; using 8-bit BGRA output. ";
else if (!outputTenBitSupported)
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
const BMDPixelFormat requestedOutputPixelFormat = DeckLinkPixelFormatForVideoIO(systemFramePixelFormat);
if (!OutputSupportsFormat(output, outputDisplayMode, requestedOutputPixelFormat))
{
error = "DeckLink output does not report support for " +
std::string(VideoIOPixelFormatName(systemFramePixelFormat)) +
" in the configured display mode.";
return false;
}
mState.outputPixelFormat = systemFramePixelFormat;
if (outputAlphaRequired)
mState.formatStatusMessage += "External keying requires alpha; using BGRA8 system frames. ";
int deckLinkOutputRowBytes = 0;
if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
@@ -291,9 +308,12 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
return false;
}
mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
mState.outputPackTextureWidth = OutputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
: mState.outputFrameSize.width;
if (OutputIsTenBit())
mState.outputPackTextureWidth = PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes);
else if (mState.outputPixelFormat == VideoIOPixelFormat::Uyvy8)
mState.outputPackTextureWidth = mState.outputFrameSize.width / 2u;
else
mState.outputPackTextureWidth = mState.outputFrameSize.width;
if (InputIsTenBit())
{

View File

@@ -42,7 +42,7 @@ public:
void ReleaseResources();
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, VideoIOPixelFormat systemFramePixelFormat, bool outputAlphaRequired, std::string& error);
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
bool PrepareOutputSchedule();
bool StartScheduledPlayback();

View File

@@ -22,7 +22,27 @@ std::string ResolveSenderName(const std::string& configuredName)
return configuredName;
}
constexpr std::size_t kBgraBytesPerPixel = 4;
bool IsSupportedNdiOutputPixelFormat(VideoIOPixelFormat pixelFormat)
{
return pixelFormat == VideoIOPixelFormat::Bgra8 || pixelFormat == VideoIOPixelFormat::Uyvy8;
}
bool NdiFourCcForPixelFormat(VideoIOPixelFormat pixelFormat, NDIlib_FourCC_video_type_e& fourCc)
{
switch (pixelFormat)
{
case VideoIOPixelFormat::Bgra8:
fourCc = NDIlib_FourCC_video_type_BGRA;
return true;
case VideoIOPixelFormat::Uyvy8:
fourCc = NDIlib_FourCC_video_type_UYVY;
return true;
case VideoIOPixelFormat::V210:
case VideoIOPixelFormat::Yuva10:
default:
return false;
}
}
void SetCommonVideoFrameFields(
NDIlib_video_frame_v2_t& ndiFrame,
@@ -30,12 +50,13 @@ void SetCommonVideoFrameFields(
unsigned int height,
int rowBytes,
void* pixels,
NDIlib_FourCC_video_type_e fourCc,
const VideoOutputEdgeConfig& config)
{
std::memset(&ndiFrame, 0, sizeof(ndiFrame));
ndiFrame.xres = static_cast<int>(width);
ndiFrame.yres = static_cast<int>(height);
ndiFrame.FourCC = NDIlib_FourCC_video_type_BGRA;
ndiFrame.FourCC = fourCc;
ndiFrame.frame_rate_N = 60000;
ndiFrame.frame_rate_D = 1001;
if (config.outputVideoMode.frameRate == "60")
@@ -80,15 +101,14 @@ void SetCommonVideoFrameFields(
ndiFrame.line_stride_in_bytes = rowBytes;
}
bool CopyBgra8FrameFlippedVertically(const VideoIOOutputFrame& frame, std::vector<unsigned char>& outputPixels, int& outputRowBytes)
bool CopyFrameFlippedVertically(const VideoIOOutputFrame& frame, std::vector<unsigned char>& outputPixels, int& outputRowBytes)
{
if (frame.bytes == nullptr || frame.width == 0 || frame.height == 0 || frame.rowBytes <= 0)
if (frame.bytes == nullptr || frame.width == 0 || frame.height == 0 || frame.rowBytes <= 0 || !IsSupportedNdiOutputPixelFormat(frame.pixelFormat))
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;
const std::size_t destinationRowBytes = VideoIORowBytes(frame.pixelFormat, frame.width);
if (sourceRowBytes < destinationRowBytes)
return false;
@@ -122,10 +142,15 @@ bool NdiOutput::Initialize(const VideoOutputEdgeConfig& config, CompletionCallba
mConfig = config;
mCompletionCallback = std::move(completionCallback);
PopulateState(mState, config, mSenderName);
if (!IsSupportedNdiOutputPixelFormat(config.systemFramePixelFormat))
{
error = std::string("NDI output does not support system frame format ") + VideoIOPixelFormatName(config.systemFramePixelFormat) + ".";
return false;
}
if (config.outputAlphaRequired)
{
mState.statusMessage = "NDI output sends BGRA frames with alpha when present; output.keying.alphaRequired is a DeckLink-only requirement.";
mState.statusMessage = "NDI output can carry BGRA alpha when configured; output.keying.alphaRequired is a DeckLink-only requirement.";
}
if (!AcquireNdiRuntime())
@@ -168,7 +193,8 @@ bool NdiOutput::StartScheduledPlayback(std::string& error)
bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
{
if (frame.bytes == nullptr || frame.pixelFormat != VideoIOPixelFormat::Bgra8)
NDIlib_FourCC_video_type_e fourCc = NDIlib_FourCC_video_type_BGRA;
if (frame.bytes == nullptr || !NdiFourCcForPixelFormat(frame.pixelFormat, fourCc))
{
++mScheduleFailures;
return false;
@@ -190,14 +216,14 @@ bool NdiOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
std::vector<unsigned char>& stagingBuffer = mStagingBuffers[mNextStagingBuffer];
int stagingRowBytes = 0;
if (!CopyBgra8FrameFlippedVertically(frame, stagingBuffer, stagingRowBytes))
if (!CopyFrameFlippedVertically(frame, stagingBuffer, stagingRowBytes))
{
++mScheduleFailures;
return false;
}
NDIlib_video_frame_v2_t ndiFrame;
SetCommonVideoFrameFields(ndiFrame, frame.width, frame.height, stagingRowBytes, stagingBuffer.data(), mConfig);
SetCommonVideoFrameFields(ndiFrame, frame.width, frame.height, stagingRowBytes, stagingBuffer.data(), fourCc, mConfig);
const auto scheduleStart = std::chrono::steady_clock::now();
NDIlib_send_send_video_async_v2(static_cast<NDIlib_send_instance_t>(sender), &ndiFrame);
@@ -303,14 +329,19 @@ void NdiOutput::CompleteBuffer(void* buffer, VideoIOCompletionResult result)
void NdiOutput::PopulateState(VideoIOState& state, const VideoOutputEdgeConfig& config, const std::string& senderName)
{
const VideoIOPixelFormat outputPixelFormat = IsSupportedNdiOutputPixelFormat(config.systemFramePixelFormat)
? config.systemFramePixelFormat
: VideoIOPixelFormat::Bgra8;
state = VideoIOState();
state.outputFrameSize = config.outputVideoMode.frameSize;
state.inputFrameSize = state.outputFrameSize;
state.outputPixelFormat = VideoIOPixelFormat::Bgra8;
state.inputPixelFormat = VideoIOPixelFormat::Bgra8;
state.outputPixelFormat = outputPixelFormat;
state.inputPixelFormat = outputPixelFormat;
state.outputFrameRowBytes = VideoIORowBytes(state.outputPixelFormat, state.outputFrameSize.width);
state.inputFrameRowBytes = state.outputFrameRowBytes;
state.outputPackTextureWidth = state.outputFrameSize.width;
state.outputPackTextureWidth = outputPixelFormat == VideoIOPixelFormat::Uyvy8
? state.outputFrameSize.width / 2u
: state.outputFrameSize.width;
state.captureTextureWidth = state.inputFrameSize.width;
state.outputDisplayModeName = config.outputVideoMode.displayName;
state.inputDisplayModeName = "No input - NDI output session";
@@ -322,6 +353,6 @@ void NdiOutput::PopulateState(VideoIOState& state, const VideoOutputEdgeConfig&
state.keyerInterfaceAvailable = false;
state.externalKeyingActive = false;
state.frameBudgetMilliseconds = config.outputVideoMode.frameDurationMilliseconds;
state.formatStatusMessage = "NDI output format: BGRA8.";
state.formatStatusMessage = std::string("NDI output format: ") + VideoIOPixelFormatName(outputPixelFormat) + ".";
}
}