Files
video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp
Aiden bfc32c4a1e
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m43s
CI / Windows Release Package (push) Has been cancelled
Phase 4
2026-05-11 18:25:47 +10:00

431 lines
12 KiB
C++

#include "VideoBackend.h"
#include "DeckLinkSession.h"
#include "OpenGLVideoIOBridge.h"
#include "HealthTelemetry.h"
#include "RenderEngine.h"
#include "RuntimeEventDispatcher.h"
#include <chrono>
#include <windows.h>
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher) :
mHealthTelemetry(healthTelemetry),
mRuntimeEventDispatcher(runtimeEventDispatcher),
mVideoIODevice(std::make_unique<DeckLinkSession>()),
mBridge(std::make_unique<OpenGLVideoIOBridge>(renderEngine))
{
}
VideoBackend::~VideoBackend()
{
ReleaseResources();
}
void VideoBackend::ReleaseResources()
{
if (mVideoIODevice)
mVideoIODevice->ReleaseResources();
}
bool VideoBackend::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
{
return mVideoIODevice->DiscoverDevicesAndModes(videoModes, error);
}
bool VideoBackend::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{
return mVideoIODevice->SelectPreferredFormats(videoModes, outputAlphaRequired, error);
}
bool VideoBackend::ConfigureInput(const VideoFormat& inputVideoMode, std::string& error)
{
return mVideoIODevice->ConfigureInput(
[this](const VideoIOFrame& frame) { HandleInputFrame(frame); },
inputVideoMode,
error);
}
bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
{
return mVideoIODevice->ConfigureOutput(
[this](const VideoIOCompletion& completion) { HandleOutputFrameCompletion(completion); },
outputVideoMode,
externalKeyingEnabled,
error);
}
bool VideoBackend::Start()
{
const bool started = mVideoIODevice->Start();
PublishBackendStateChanged(started ? "started" : "start-failed", started ? "Video backend started." : StatusMessage());
return started;
}
bool VideoBackend::Stop()
{
const bool stopped = mVideoIODevice->Stop();
PublishBackendStateChanged(stopped ? "stopped" : "stop-failed", stopped ? "Video backend stopped." : StatusMessage());
return stopped;
}
const VideoIOState& VideoBackend::State() const
{
return mVideoIODevice->State();
}
VideoIOState& VideoBackend::MutableState()
{
return mVideoIODevice->MutableState();
}
bool VideoBackend::BeginOutputFrame(VideoIOOutputFrame& frame)
{
return mVideoIODevice->BeginOutputFrame(frame);
}
void VideoBackend::EndOutputFrame(VideoIOOutputFrame& frame)
{
mVideoIODevice->EndOutputFrame(frame);
}
bool VideoBackend::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
{
return mVideoIODevice->ScheduleOutputFrame(frame);
}
void VideoBackend::AccountForCompletionResult(VideoIOCompletionResult result)
{
mVideoIODevice->AccountForCompletionResult(result);
}
bool VideoBackend::HasInputDevice() const
{
return mVideoIODevice->HasInputDevice();
}
bool VideoBackend::HasInputSource() const
{
return mVideoIODevice->HasInputSource();
}
unsigned VideoBackend::InputFrameWidth() const
{
return mVideoIODevice->InputFrameWidth();
}
unsigned VideoBackend::InputFrameHeight() const
{
return mVideoIODevice->InputFrameHeight();
}
unsigned VideoBackend::OutputFrameWidth() const
{
return mVideoIODevice->OutputFrameWidth();
}
unsigned VideoBackend::OutputFrameHeight() const
{
return mVideoIODevice->OutputFrameHeight();
}
unsigned VideoBackend::CaptureTextureWidth() const
{
return mVideoIODevice->CaptureTextureWidth();
}
unsigned VideoBackend::OutputPackTextureWidth() const
{
return mVideoIODevice->OutputPackTextureWidth();
}
VideoIOPixelFormat VideoBackend::InputPixelFormat() const
{
return mVideoIODevice->InputPixelFormat();
}
const std::string& VideoBackend::InputDisplayModeName() const
{
return mVideoIODevice->InputDisplayModeName();
}
const std::string& VideoBackend::OutputModelName() const
{
return mVideoIODevice->OutputModelName();
}
bool VideoBackend::SupportsInternalKeying() const
{
return mVideoIODevice->SupportsInternalKeying();
}
bool VideoBackend::SupportsExternalKeying() const
{
return mVideoIODevice->SupportsExternalKeying();
}
bool VideoBackend::KeyerInterfaceAvailable() const
{
return mVideoIODevice->KeyerInterfaceAvailable();
}
bool VideoBackend::ExternalKeyingActive() const
{
return mVideoIODevice->ExternalKeyingActive();
}
const std::string& VideoBackend::StatusMessage() const
{
return mVideoIODevice->StatusMessage();
}
void VideoBackend::SetStatusMessage(const std::string& message)
{
mVideoIODevice->SetStatusMessage(message);
}
void VideoBackend::PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage)
{
if (!statusMessage.empty())
SetStatusMessage(statusMessage);
mHealthTelemetry.ReportVideoIOStatus(
"decklink",
OutputModelName(),
SupportsInternalKeying(),
SupportsExternalKeying(),
KeyerInterfaceAvailable(),
externalKeyingConfigured,
ExternalKeyingActive(),
StatusMessage());
PublishBackendStateChanged("status", StatusMessage());
}
void VideoBackend::ReportNoInputDeviceSignalStatus()
{
mHealthTelemetry.ReportSignalStatus(
false,
InputFrameWidth(),
InputFrameHeight(),
InputDisplayModeName());
PublishBackendStateChanged("no-input-device", "No input device is available.");
}
void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
{
const VideoIOState& state = mVideoIODevice->State();
mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
PublishInputSignalChanged(frame, state);
PublishInputFrameArrived(frame);
if (mBridge)
mBridge->UploadInputFrame(frame, state);
}
void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion)
{
RecordFramePacing(completion.result);
PublishOutputFrameCompleted(completion);
VideoIOOutputFrame outputFrame;
if (!BeginOutputFrame(outputFrame))
return;
const VideoIOState& state = mVideoIODevice->State();
bool rendered = true;
if (mBridge)
rendered = mBridge->RenderScheduledFrame(state, completion, outputFrame);
EndOutputFrame(outputFrame);
AccountForCompletionResult(completion.result);
if (!rendered)
{
PublishBackendStateChanged("output-render-failed", "Output frame render request failed; skipping schedule for this frame.");
return;
}
// Schedule the next frame after render work is complete so device-side
// bookkeeping stays with the backend seam and the bridge stays render-only.
if (ScheduleOutputFrame(outputFrame))
PublishOutputFrameScheduled(outputFrame);
}
void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult)
{
const auto now = std::chrono::steady_clock::now();
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
{
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
else
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
}
mLastPlayoutCompletionTime = now;
if (completionResult == VideoIOCompletionResult::DisplayedLate)
++mLateFrameCount;
else if (completionResult == VideoIOCompletionResult::Dropped)
++mDroppedFrameCount;
else if (completionResult == VideoIOCompletionResult::Flushed)
++mFlushedFrameCount;
mHealthTelemetry.TryRecordFramePacingStats(
mCompletionIntervalMilliseconds,
mSmoothedCompletionIntervalMilliseconds,
mMaxCompletionIntervalMilliseconds,
mLateFrameCount,
mDroppedFrameCount,
mFlushedFrameCount);
PublishTimingSample("VideoBackend", "completionInterval", mCompletionIntervalMilliseconds, "ms");
PublishTimingSample("VideoBackend", "smoothedCompletionInterval", mSmoothedCompletionIntervalMilliseconds, "ms");
}
void VideoBackend::PublishBackendStateChanged(const std::string& state, const std::string& message)
{
try
{
BackendStateChangedEvent event;
event.backendName = "decklink";
event.state = state;
event.message = message;
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
OutputDebugStringA("BackendStateChanged event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("BackendStateChanged event publish threw.\n");
}
}
void VideoBackend::PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state)
{
const bool hasSignal = !frame.hasNoInputSource;
const unsigned width = state.inputFrameSize.width;
const unsigned height = state.inputFrameSize.height;
if (mHasLastInputSignal &&
mLastInputSignal == hasSignal &&
mLastInputSignalWidth == width &&
mLastInputSignalHeight == height &&
mLastInputSignalModeName == state.inputDisplayModeName)
{
return;
}
mHasLastInputSignal = true;
mLastInputSignal = hasSignal;
mLastInputSignalWidth = width;
mLastInputSignalHeight = height;
mLastInputSignalModeName = state.inputDisplayModeName;
try
{
InputSignalChangedEvent event;
event.hasSignal = hasSignal;
event.width = width;
event.height = height;
event.modeName = state.inputDisplayModeName;
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
OutputDebugStringA("InputSignalChanged event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("InputSignalChanged event publish threw.\n");
}
}
void VideoBackend::PublishInputFrameArrived(const VideoIOFrame& frame)
{
try
{
InputFrameArrivedEvent event;
event.frameIndex = ++mInputFrameIndex;
event.width = frame.width;
event.height = frame.height;
event.rowBytes = frame.rowBytes;
event.pixelFormat = PixelFormatName(frame.pixelFormat);
event.hasNoInputSource = frame.hasNoInputSource;
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
OutputDebugStringA("InputFrameArrived event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("InputFrameArrived event publish threw.\n");
}
}
void VideoBackend::PublishOutputFrameScheduled(const VideoIOOutputFrame& frame)
{
try
{
OutputFrameScheduledEvent event;
event.frameIndex = ++mOutputFrameScheduleIndex;
(void)frame;
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
OutputDebugStringA("OutputFrameScheduled event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("OutputFrameScheduled event publish threw.\n");
}
}
void VideoBackend::PublishOutputFrameCompleted(const VideoIOCompletion& completion)
{
try
{
OutputFrameCompletedEvent event;
event.frameIndex = ++mOutputFrameCompletionIndex;
event.result = CompletionResultName(completion.result);
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
OutputDebugStringA("OutputFrameCompleted event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("OutputFrameCompleted event publish threw.\n");
}
}
void VideoBackend::PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit)
{
try
{
TimingSampleRecordedEvent event;
event.subsystem = subsystem;
event.metric = metric;
event.value = value;
event.unit = unit;
if (!mRuntimeEventDispatcher.PublishPayload(event, "HealthTelemetry"))
OutputDebugStringA("TimingSampleRecorded event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("TimingSampleRecorded event publish threw.\n");
}
}
std::string VideoBackend::CompletionResultName(VideoIOCompletionResult result)
{
switch (result)
{
case VideoIOCompletionResult::Completed:
return "Completed";
case VideoIOCompletionResult::DisplayedLate:
return "DisplayedLate";
case VideoIOCompletionResult::Dropped:
return "Dropped";
case VideoIOCompletionResult::Flushed:
return "Flushed";
case VideoIOCompletionResult::Unknown:
default:
return "Unknown";
}
}
std::string VideoBackend::PixelFormatName(VideoIOPixelFormat pixelFormat)
{
return std::string(VideoIOPixelFormatName(pixelFormat));
}