videoIO seperation

This commit is contained in:
2026-05-22 14:58:42 +10:00
parent 35801601a5
commit b7e7452567
12 changed files with 401 additions and 187 deletions

View File

@@ -29,9 +29,9 @@ SystemFrameExchange
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
protects scheduled frames until DeckLink completion
DeckLinkOutputThread
VideoOutputThread
consumes completed system-memory frames
schedules them into DeckLink up to target depth
schedules them into the active video output edge up to target depth
never renders
PreviewWindowThread
@@ -47,7 +47,7 @@ Startup builds a small output preroll reserve before DeckLink scheduled playback
Included now:
- output-only DeckLink
- generic video output edge contract with DeckLink as the current implementation
- optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
- non-blocking startup when DeckLink output is unavailable
- hidden render-thread-owned OpenGL context

View File

@@ -203,7 +203,7 @@ int main(int argc, char** argv)
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
app.SetVideoInputMetricsProvider([&deckLinkInput]() {
return deckLinkInput.Metrics();
});

View File

@@ -5,7 +5,7 @@
#include "../preview/PreviewConfig.h"
#include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h"
#include "../video/VideoOutputThread.h"
#include <chrono>
#include <cstddef>
@@ -16,7 +16,7 @@ namespace RenderCadenceCompositor
struct AppConfig
{
DeckLinkOutputConfig deckLink;
DeckLinkOutputThreadConfig outputThread;
VideoOutputThreadConfig outputThread;
TelemetryHealthMonitorConfig telemetry;
LoggerConfig logging;
HttpControlServerConfig http;

View File

@@ -7,9 +7,9 @@
#include "../control/RuntimeStateJson.h"
#include "../preview/PreviewWindowThread.h"
#include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkInput.h"
#include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h"
#include "../video/VideoIOEdges.h"
#include "../video/VideoOutputThread.h"
#include <chrono>
#include <filesystem>
@@ -56,7 +56,10 @@ public:
mRenderThread(renderThread),
mFrameExchange(frameExchange),
mConfig(config),
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
mOutputThread(mOutput, mFrameExchange, VideoOutputThreadConfig{
mConfig.outputThread.targetBufferedFrames,
mConfig.outputThread.idleSleep
}),
mTelemetryHealth(mConfig.telemetry),
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
mRenderThread.SubmitRuntimeRenderLayers(layers);
@@ -120,10 +123,10 @@ public:
}
bool Started() const { return mStarted; }
const DeckLinkOutput& Output() const { return mOutput; }
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
const IVideoOutputEdge& Output() const { return mOutput; }
void SetVideoInputMetricsProvider(std::function<VideoInputEdgeMetrics()> provider)
{
mDeckLinkInputMetricsProvider = std::move(provider);
mVideoInputMetricsProvider = std::move(provider);
}
private:
@@ -142,10 +145,10 @@ private:
return;
}
Log("app", "Starting DeckLink output thread.");
Log("app", "Starting video output thread.");
if (!mOutputThread.Start())
{
DisableVideoOutput("DeckLink output thread failed to start.");
DisableVideoOutput("Video output thread failed to start.");
return;
}
@@ -252,7 +255,7 @@ private:
std::string BuildStateJson()
{
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
ApplyDeckLinkInputMetrics(telemetry);
ApplyVideoInputMetrics(telemetry);
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
return RuntimeStateToJson(RuntimeStateJsonInput{
mConfig,
@@ -265,12 +268,12 @@ private:
});
}
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
void ApplyVideoInputMetrics(CadenceTelemetrySnapshot& telemetry)
{
if (!mDeckLinkInputMetricsProvider)
if (!mVideoInputMetricsProvider)
return;
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
const VideoInputEdgeMetrics inputMetrics = mVideoInputMetricsProvider();
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
@@ -298,13 +301,13 @@ private:
SystemFrameExchange& mFrameExchange;
AppConfig mConfig;
DeckLinkOutput mOutput;
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
VideoOutputThread<SystemFrameExchange> mOutputThread;
TelemetryHealthMonitor mTelemetryHealth;
CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer;
PreviewWindowThread mPreviewWindow;
RuntimeLayerController mRuntimeLayers;
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
std::function<VideoInputEdgeMetrics()> mVideoInputMetricsProvider;
uint64_t mLastInputCapturedFrames = 0;
bool mStarted = false;
bool mVideoOutputEnabled = false;

View File

@@ -3,6 +3,7 @@
#include "../frames/InputFrameMailbox.h"
#include "DeckLinkAPI_h.h"
#include "DeckLinkDisplayMode.h"
#include "VideoIOEdges.h"
#include <atlbase.h>
@@ -19,16 +20,7 @@ struct DeckLinkInputConfig
VideoFormat videoFormat;
};
struct DeckLinkInputMetrics
{
uint64_t capturedFrames = 0;
uint64_t noInputSourceFrames = 0;
uint64_t unsupportedFrames = 0;
uint64_t submitMisses = 0;
double convertMilliseconds = 0.0;
double submitMilliseconds = 0.0;
const char* captureFormat = "none";
};
using DeckLinkInputMetrics = VideoInputEdgeMetrics;
class DeckLinkInput;
@@ -48,7 +40,7 @@ private:
std::atomic<ULONG> mRefCount{ 1 };
};
class DeckLinkInput
class DeckLinkInput : public IVideoInputEdge
{
public:
DeckLinkInput(InputFrameMailbox& mailbox);
@@ -57,14 +49,14 @@ public:
~DeckLinkInput();
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
bool Start(std::string& error);
void Stop();
void ReleaseResources();
bool Start(std::string& error) override;
void Stop() override;
void ReleaseResources() override;
bool IsInitialized() const { return mInput != nullptr; }
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
VideoIOPixelFormat CapturePixelFormat() const;
DeckLinkInputMetrics Metrics() const;
bool IsInitialized() const override { return mInput != nullptr; }
bool IsRunning() const override { return mRunning.load(std::memory_order_acquire); }
VideoIOPixelFormat CapturePixelFormat() const override;
DeckLinkInputMetrics Metrics() const override;
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
void HandleFormatChanged();

View File

@@ -2,6 +2,7 @@
#include "DeckLinkDisplayMode.h"
#include "DeckLinkSession.h"
#include "VideoIOEdges.h"
#include "VideoIOTypes.h"
#include <atomic>
@@ -18,28 +19,12 @@ struct DeckLinkOutputConfig
bool outputAlphaRequired = false;
};
struct DeckLinkOutputMetrics
{
uint64_t completions = 0;
uint64_t displayedLate = 0;
uint64_t dropped = 0;
uint64_t flushed = 0;
uint64_t scheduleFailures = 0;
bool actualBufferedFramesAvailable = false;
uint64_t actualBufferedFrames = 0;
double scheduleCallMilliseconds = 0.0;
bool scheduleLeadAvailable = false;
int64_t playbackStreamTime = 0;
uint64_t playbackFrameIndex = 0;
uint64_t nextScheduleFrameIndex = 0;
int64_t scheduleLeadFrames = 0;
uint64_t scheduleRealignmentCount = 0;
};
using DeckLinkOutputMetrics = VideoOutputEdgeMetrics;
class DeckLinkOutput
class DeckLinkOutput : public IVideoOutputEdge
{
public:
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
using CompletionCallback = IVideoOutputEdge::CompletionCallback;
DeckLinkOutput() = default;
DeckLinkOutput(const DeckLinkOutput&) = delete;
@@ -47,13 +32,13 @@ public:
~DeckLinkOutput();
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
bool StartScheduledPlayback(std::string& error);
bool ScheduleFrame(const VideoIOOutputFrame& frame);
void Stop();
void ReleaseResources();
bool StartScheduledPlayback(std::string& error) override;
bool ScheduleFrame(const VideoIOOutputFrame& frame) override;
void Stop() override;
void ReleaseResources() override;
const VideoIOState& State() const;
DeckLinkOutputMetrics Metrics() const;
const VideoIOState& State() const override;
DeckLinkOutputMetrics Metrics() const override;
private:
void HandleCompletion(const VideoIOCompletion& completion);

View File

@@ -1,127 +1,12 @@
#pragma once
#include "../frames/SystemFrameTypes.h"
#include "DeckLinkOutput.h"
#include "VideoIOTypes.h"
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <thread>
#include "VideoOutputThread.h"
namespace RenderCadenceCompositor
{
struct DeckLinkOutputThreadConfig
{
std::size_t targetBufferedFrames = 4;
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
};
struct DeckLinkOutputThreadMetrics
{
uint64_t scheduledFrames = 0;
uint64_t completedPollMisses = 0;
uint64_t scheduleFailures = 0;
};
using DeckLinkOutputThreadConfig = VideoOutputThreadConfig;
using DeckLinkOutputThreadMetrics = VideoOutputThreadMetrics;
template <typename SystemFrameExchange>
class DeckLinkOutputThread
{
public:
DeckLinkOutputThread(DeckLinkOutput& output, SystemFrameExchange& exchange, DeckLinkOutputThreadConfig config = DeckLinkOutputThreadConfig()) :
mOutput(output),
mExchange(exchange),
mConfig(config)
{
}
DeckLinkOutputThread(const DeckLinkOutputThread&) = delete;
DeckLinkOutputThread& operator=(const DeckLinkOutputThread&) = delete;
~DeckLinkOutputThread()
{
Stop();
}
bool Start()
{
if (mRunning)
return true;
mStopping = false;
mThread = std::thread([this]() { ThreadMain(); });
mRunning = true;
return true;
}
void Stop()
{
mStopping = true;
if (mThread.joinable())
mThread.join();
mRunning = false;
}
DeckLinkOutputThreadMetrics Metrics() const
{
DeckLinkOutputThreadMetrics metrics;
metrics.scheduledFrames = mScheduledFrames.load();
metrics.completedPollMisses = mCompletedPollMisses.load();
metrics.scheduleFailures = mScheduleFailures.load();
return metrics;
}
private:
void ThreadMain()
{
while (!mStopping)
{
const auto exchangeMetrics = mExchange.Metrics();
const auto outputMetrics = mOutput.Metrics();
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
: exchangeMetrics.scheduledCount;
if (bufferedFrames >= mConfig.targetBufferedFrames)
{
std::this_thread::sleep_for(mConfig.idleSleep);
continue;
}
SystemFrame frame;
if (!mExchange.ConsumeCompletedForSchedule(frame))
{
++mCompletedPollMisses;
std::this_thread::sleep_for(mConfig.idleSleep);
continue;
}
VideoIOOutputFrame outputFrame;
outputFrame.bytes = frame.bytes;
outputFrame.nativeBuffer = frame.bytes;
outputFrame.rowBytes = frame.rowBytes;
outputFrame.width = frame.width;
outputFrame.height = frame.height;
outputFrame.pixelFormat = frame.pixelFormat;
if (!mOutput.ScheduleFrame(outputFrame))
{
++mScheduleFailures;
mExchange.ReleaseScheduledByBytes(frame.bytes);
std::this_thread::sleep_for(mConfig.idleSleep);
continue;
}
++mScheduledFrames;
}
}
DeckLinkOutput& mOutput;
SystemFrameExchange& mExchange;
DeckLinkOutputThreadConfig mConfig;
std::thread mThread;
std::atomic<bool> mStopping{ false };
std::atomic<bool> mRunning{ false };
std::atomic<uint64_t> mScheduledFrames{ 0 };
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
std::atomic<uint64_t> mScheduleFailures{ 0 };
};
using DeckLinkOutputThread = VideoOutputThread<SystemFrameExchange>;
}

69
src/video/VideoIOEdges.h Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include "VideoIOFormat.h"
#include "VideoIOTypes.h"
#include <cstdint>
#include <functional>
#include <string>
namespace RenderCadenceCompositor
{
struct VideoInputEdgeMetrics
{
uint64_t capturedFrames = 0;
uint64_t noInputSourceFrames = 0;
uint64_t unsupportedFrames = 0;
uint64_t submitMisses = 0;
double convertMilliseconds = 0.0;
double submitMilliseconds = 0.0;
const char* captureFormat = "none";
};
class IVideoInputEdge
{
public:
virtual ~IVideoInputEdge() = default;
virtual bool Start(std::string& error) = 0;
virtual void Stop() = 0;
virtual void ReleaseResources() = 0;
virtual bool IsInitialized() const = 0;
virtual bool IsRunning() const = 0;
virtual VideoIOPixelFormat CapturePixelFormat() const = 0;
virtual VideoInputEdgeMetrics Metrics() const = 0;
};
struct VideoOutputEdgeMetrics
{
uint64_t completions = 0;
uint64_t displayedLate = 0;
uint64_t dropped = 0;
uint64_t flushed = 0;
uint64_t scheduleFailures = 0;
bool actualBufferedFramesAvailable = false;
uint64_t actualBufferedFrames = 0;
double scheduleCallMilliseconds = 0.0;
bool scheduleLeadAvailable = false;
int64_t playbackStreamTime = 0;
uint64_t playbackFrameIndex = 0;
uint64_t nextScheduleFrameIndex = 0;
int64_t scheduleLeadFrames = 0;
uint64_t scheduleRealignmentCount = 0;
};
class IVideoOutputEdge
{
public:
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
virtual ~IVideoOutputEdge() = default;
virtual bool StartScheduledPlayback(std::string& error) = 0;
virtual bool ScheduleFrame(const VideoIOOutputFrame& frame) = 0;
virtual void Stop() = 0;
virtual void ReleaseResources() = 0;
virtual const VideoIOState& State() const = 0;
virtual VideoOutputEdgeMetrics Metrics() const = 0;
};
}

View File

@@ -0,0 +1,127 @@
#pragma once
#include "../frames/SystemFrameTypes.h"
#include "VideoIOEdges.h"
#include "VideoIOTypes.h"
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <thread>
namespace RenderCadenceCompositor
{
struct VideoOutputThreadConfig
{
std::size_t targetBufferedFrames = 4;
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
};
struct VideoOutputThreadMetrics
{
uint64_t scheduledFrames = 0;
uint64_t completedPollMisses = 0;
uint64_t scheduleFailures = 0;
};
template <typename SystemFrameExchange>
class VideoOutputThread
{
public:
VideoOutputThread(IVideoOutputEdge& output, SystemFrameExchange& exchange, VideoOutputThreadConfig config = VideoOutputThreadConfig()) :
mOutput(output),
mExchange(exchange),
mConfig(config)
{
}
VideoOutputThread(const VideoOutputThread&) = delete;
VideoOutputThread& operator=(const VideoOutputThread&) = delete;
~VideoOutputThread()
{
Stop();
}
bool Start()
{
if (mRunning)
return true;
mStopping = false;
mThread = std::thread([this]() { ThreadMain(); });
mRunning = true;
return true;
}
void Stop()
{
mStopping = true;
if (mThread.joinable())
mThread.join();
mRunning = false;
}
VideoOutputThreadMetrics Metrics() const
{
VideoOutputThreadMetrics metrics;
metrics.scheduledFrames = mScheduledFrames.load();
metrics.completedPollMisses = mCompletedPollMisses.load();
metrics.scheduleFailures = mScheduleFailures.load();
return metrics;
}
private:
void ThreadMain()
{
while (!mStopping)
{
const auto exchangeMetrics = mExchange.Metrics();
const auto outputMetrics = mOutput.Metrics();
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
: exchangeMetrics.scheduledCount;
if (bufferedFrames >= mConfig.targetBufferedFrames)
{
std::this_thread::sleep_for(mConfig.idleSleep);
continue;
}
SystemFrame frame;
if (!mExchange.ConsumeCompletedForSchedule(frame))
{
++mCompletedPollMisses;
std::this_thread::sleep_for(mConfig.idleSleep);
continue;
}
VideoIOOutputFrame outputFrame;
outputFrame.bytes = frame.bytes;
outputFrame.nativeBuffer = frame.bytes;
outputFrame.rowBytes = frame.rowBytes;
outputFrame.width = frame.width;
outputFrame.height = frame.height;
outputFrame.pixelFormat = frame.pixelFormat;
if (!mOutput.ScheduleFrame(outputFrame))
{
++mScheduleFailures;
mExchange.ReleaseScheduledByBytes(frame.bytes);
std::this_thread::sleep_for(mConfig.idleSleep);
continue;
}
++mScheduledFrames;
}
}
IVideoOutputEdge& mOutput;
SystemFrameExchange& mExchange;
VideoOutputThreadConfig mConfig;
std::thread mThread;
std::atomic<bool> mStopping{ false };
std::atomic<bool> mRunning{ false };
std::atomic<uint64_t> mScheduledFrames{ 0 };
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
std::atomic<uint64_t> mScheduleFailures{ 0 };
};
}