1096 lines
35 KiB
C++
1096 lines
35 KiB
C++
#include "VideoBackend.h"
|
|
|
|
#include "DeckLinkSession.h"
|
|
#include "OpenGLVideoIOBridge.h"
|
|
#include "HealthTelemetry.h"
|
|
#include "RenderEngine.h"
|
|
#include "RuntimeEventDispatcher.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cmath>
|
|
#include <windows.h>
|
|
|
|
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
|
mHealthTelemetry(healthTelemetry),
|
|
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
|
mPlayoutPolicy(NormalizeVideoPlayoutPolicy(VideoPlayoutPolicy())),
|
|
mOutputProductionController(mPlayoutPolicy),
|
|
mReadyOutputQueue(mPlayoutPolicy),
|
|
mVideoIODevice(std::make_unique<DeckLinkSession>()),
|
|
mBridge(std::make_unique<OpenGLVideoIOBridge>(renderEngine)),
|
|
mInputCaptureDisabled(IsEnvironmentFlagEnabled("VST_DISABLE_INPUT_CAPTURE"))
|
|
{
|
|
}
|
|
|
|
VideoBackend::~VideoBackend()
|
|
{
|
|
ReleaseResources();
|
|
}
|
|
|
|
void VideoBackend::ReleaseResources()
|
|
{
|
|
StopOutputCompletionWorker();
|
|
mReadyOutputQueue.Clear();
|
|
if (mVideoIODevice)
|
|
mVideoIODevice->ReleaseResources();
|
|
mSystemOutputFramePool.Clear();
|
|
if (!VideoBackendLifecycle::CanTransition(mLifecycle.State(), VideoBackendLifecycleState::Stopped))
|
|
ApplyLifecycleFailure("Video backend resources released before lifecycle completed.");
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Stopped, "Video backend resources released.");
|
|
}
|
|
|
|
VideoBackendLifecycleState VideoBackend::LifecycleState() const
|
|
{
|
|
return mLifecycle.State();
|
|
}
|
|
|
|
bool VideoBackend::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
|
{
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Discovering, "Discovering video backend devices and modes.");
|
|
if (mVideoIODevice->DiscoverDevicesAndModes(videoModes, error))
|
|
return ApplyLifecycleTransition(VideoBackendLifecycleState::Discovered, "Video backend devices and modes discovered.");
|
|
|
|
ApplyLifecycleFailure(error.empty() ? "Video backend discovery failed." : error);
|
|
return false;
|
|
}
|
|
|
|
bool VideoBackend::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
|
{
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Configuring, "Selecting preferred video backend formats.");
|
|
if (mVideoIODevice->SelectPreferredFormats(videoModes, outputAlphaRequired, error))
|
|
return true;
|
|
|
|
ApplyLifecycleFailure(error.empty() ? "Video backend format selection failed." : error);
|
|
return false;
|
|
}
|
|
|
|
bool VideoBackend::ConfigureInput(const VideoFormat& inputVideoMode, std::string& error)
|
|
{
|
|
if (mLifecycle.State() != VideoBackendLifecycleState::Configuring)
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Configuring, "Configuring video backend input.");
|
|
if (mInputCaptureDisabled)
|
|
{
|
|
MutableState().hasInputSource = false;
|
|
MutableState().statusMessage = "DeckLink input capture disabled by VST_DISABLE_INPUT_CAPTURE for output timing isolation.";
|
|
return true;
|
|
}
|
|
if (!mVideoIODevice->ConfigureInput(
|
|
[this](const VideoIOFrame& frame) { HandleInputFrame(frame); },
|
|
inputVideoMode,
|
|
error))
|
|
{
|
|
ApplyLifecycleFailure(error.empty() ? "Video backend input configuration failed." : error);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
|
{
|
|
mPlayoutPolicy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
|
mOutputProductionController.Configure(mPlayoutPolicy);
|
|
mReadyOutputQueue.Configure(mPlayoutPolicy);
|
|
if (mLifecycle.State() != VideoBackendLifecycleState::Configuring)
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Configuring, "Configuring video backend output.");
|
|
if (!mVideoIODevice->ConfigureOutput(
|
|
[this](const VideoIOCompletion& completion) { HandleOutputFrameCompletion(completion); },
|
|
outputVideoMode,
|
|
externalKeyingEnabled,
|
|
error))
|
|
{
|
|
ApplyLifecycleFailure(error.empty() ? "Video backend output configuration failed." : error);
|
|
return false;
|
|
}
|
|
SystemOutputFramePoolConfig poolConfig;
|
|
poolConfig.width = mVideoIODevice->OutputFrameWidth();
|
|
poolConfig.height = mVideoIODevice->OutputFrameHeight();
|
|
poolConfig.pixelFormat = mVideoIODevice->OutputPixelFormat();
|
|
poolConfig.rowBytes = mVideoIODevice->OutputFrameRowBytes();
|
|
poolConfig.capacity = mPlayoutPolicy.outputFramePoolSize;
|
|
mSystemOutputFramePool.Configure(poolConfig);
|
|
RecordSystemMemoryPlayoutStats();
|
|
return ApplyLifecycleTransition(VideoBackendLifecycleState::Configured, "Video backend configured.");
|
|
}
|
|
|
|
bool VideoBackend::Start()
|
|
{
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Prerolling, "Video backend preroll starting.");
|
|
if (!mVideoIODevice->PrepareOutputSchedule())
|
|
{
|
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend output schedule preparation failed." : StatusMessage());
|
|
return false;
|
|
}
|
|
|
|
StartOutputCompletionWorker();
|
|
StartOutputProducerWorker();
|
|
|
|
if (!WarmupOutputPreroll())
|
|
{
|
|
StopOutputProducerWorker();
|
|
StopOutputCompletionWorker();
|
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend preroll warmup failed." : StatusMessage());
|
|
return false;
|
|
}
|
|
|
|
if (!mInputCaptureDisabled && !mVideoIODevice->StartInputStreams())
|
|
{
|
|
StopOutputProducerWorker();
|
|
StopOutputCompletionWorker();
|
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend input stream start failed." : StatusMessage());
|
|
return false;
|
|
}
|
|
|
|
if (!mVideoIODevice->StartScheduledPlayback())
|
|
{
|
|
StopOutputProducerWorker();
|
|
mVideoIODevice->Stop();
|
|
StopOutputCompletionWorker();
|
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend scheduled playback start failed." : StatusMessage());
|
|
return false;
|
|
}
|
|
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Running, "Video backend started.");
|
|
return true;
|
|
}
|
|
|
|
bool VideoBackend::Stop()
|
|
{
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Stopping, "Video backend stopping.");
|
|
StopOutputProducerWorker();
|
|
const bool stopped = mVideoIODevice->Stop();
|
|
StopOutputCompletionWorker();
|
|
if (stopped)
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Stopped, "Video backend stopped.");
|
|
else
|
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend stop failed." : 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);
|
|
}
|
|
|
|
VideoPlayoutRecoveryDecision VideoBackend::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
|
{
|
|
return mVideoIODevice->AccountForCompletionResult(result, readyQueueDepth);
|
|
}
|
|
|
|
bool VideoBackend::HasInputDevice() const
|
|
{
|
|
return mVideoIODevice->HasInputDevice();
|
|
}
|
|
|
|
bool VideoBackend::HasInputSource() const
|
|
{
|
|
if (mInputCaptureDisabled)
|
|
return false;
|
|
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();
|
|
}
|
|
|
|
bool VideoBackend::ShouldPrioritizeOutputOverPreview() const
|
|
{
|
|
const RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
|
return metrics.depth < static_cast<std::size_t>(mPlayoutPolicy.targetReadyFrames);
|
|
}
|
|
|
|
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(VideoBackendLifecycle::StateName(mLifecycle.State()), 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)
|
|
{
|
|
if (mInputCaptureDisabled)
|
|
return;
|
|
|
|
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)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputCompletionMutex);
|
|
if (!mOutputCompletionWorkerRunning || mOutputCompletionWorkerStopping)
|
|
return;
|
|
mPendingOutputCompletions.push_back(completion);
|
|
}
|
|
mOutputCompletionCondition.notify_one();
|
|
}
|
|
|
|
void VideoBackend::StartOutputCompletionWorker()
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputCompletionMutex);
|
|
if (mOutputCompletionWorkerRunning)
|
|
return;
|
|
|
|
mPendingOutputCompletions.clear();
|
|
mReadyOutputQueue.Clear();
|
|
mNextReadyOutputFrameIndex = 0;
|
|
mHasReadyQueueDepthBaseline = false;
|
|
mMinReadyQueueDepth = 0;
|
|
mMaxReadyQueueDepth = 0;
|
|
mReadyQueueZeroDepthCount = 0;
|
|
mOutputRenderMilliseconds = 0.0;
|
|
mSmoothedOutputRenderMilliseconds = 0.0;
|
|
mMaxOutputRenderMilliseconds = 0.0;
|
|
mOutputFrameAcquireMilliseconds = 0.0;
|
|
mOutputFrameRenderRequestMilliseconds = 0.0;
|
|
mOutputFrameEndAccessMilliseconds = 0.0;
|
|
mLastLateStreak = 0;
|
|
mLastDropStreak = 0;
|
|
mOutputCompletionWorkerStopping = false;
|
|
mOutputCompletionWorkerRunning = true;
|
|
mOutputCompletionWorker = std::thread(&VideoBackend::OutputCompletionWorkerMain, this);
|
|
}
|
|
}
|
|
|
|
void VideoBackend::StopOutputCompletionWorker()
|
|
{
|
|
StopOutputProducerWorker();
|
|
|
|
bool shouldJoin = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputCompletionMutex);
|
|
if (mOutputCompletionWorkerRunning)
|
|
mOutputCompletionWorkerStopping = true;
|
|
shouldJoin = mOutputCompletionWorker.joinable();
|
|
}
|
|
mOutputCompletionCondition.notify_one();
|
|
|
|
if (shouldJoin)
|
|
mOutputCompletionWorker.join();
|
|
}
|
|
|
|
void VideoBackend::StartOutputProducerWorker()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
|
if (mOutputProducerWorkerRunning)
|
|
return;
|
|
|
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds;
|
|
const auto frameDuration = frameBudgetMilliseconds > 0.0
|
|
? std::chrono::duration_cast<RenderCadenceController::Duration>(
|
|
std::chrono::duration<double, std::milli>(frameBudgetMilliseconds))
|
|
: std::chrono::milliseconds(16);
|
|
mRenderCadenceController.Configure(frameDuration, std::chrono::steady_clock::now());
|
|
mLastOutputProductionCompletion = VideoIOCompletion();
|
|
mLastOutputProductionTime = std::chrono::steady_clock::time_point();
|
|
mOutputProducerWorkerStopping = false;
|
|
mOutputProducerWorkerRunning = true;
|
|
mOutputProducerWorker = std::thread(&VideoBackend::OutputProducerWorkerMain, this);
|
|
mOutputProducerCondition.notify_one();
|
|
}
|
|
|
|
void VideoBackend::StopOutputProducerWorker()
|
|
{
|
|
bool shouldJoin = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
|
if (mOutputProducerWorkerRunning)
|
|
mOutputProducerWorkerStopping = true;
|
|
shouldJoin = mOutputProducerWorker.joinable();
|
|
}
|
|
mOutputProducerCondition.notify_one();
|
|
|
|
if (shouldJoin)
|
|
mOutputProducerWorker.join();
|
|
}
|
|
|
|
void VideoBackend::NotifyOutputProducer()
|
|
{
|
|
mOutputProducerCondition.notify_one();
|
|
}
|
|
|
|
bool VideoBackend::WarmupOutputPreroll()
|
|
{
|
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
|
const std::size_t targetPrerollFrames = static_cast<std::size_t>(policy.targetPrerollFrames);
|
|
if (targetPrerollFrames == 0)
|
|
return true;
|
|
|
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds > 0.0 ? State().frameBudgetMilliseconds : 16.0;
|
|
const auto estimatedCadenceTime = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::duration<double, std::milli>(frameBudgetMilliseconds * static_cast<double>(targetPrerollFrames + 2)));
|
|
const auto timeout = (std::max)(std::chrono::milliseconds(1000), estimatedCadenceTime + std::chrono::milliseconds(500));
|
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
|
|
|
while (std::chrono::steady_clock::now() < deadline)
|
|
{
|
|
ScheduleReadyOutputFramesToTarget();
|
|
const SystemOutputFramePoolMetrics metrics = mSystemOutputFramePool.GetMetrics();
|
|
RecordSystemMemoryPlayoutStats();
|
|
if (metrics.scheduledCount >= targetPrerollFrames)
|
|
return true;
|
|
|
|
NotifyOutputProducer();
|
|
const auto waitDuration = (std::min)(OutputProducerWakeInterval(), std::chrono::milliseconds(5));
|
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
|
mOutputProducerCondition.wait_for(lock, waitDuration);
|
|
if (mOutputProducerWorkerStopping)
|
|
return false;
|
|
}
|
|
|
|
SetStatusMessage("Timed out warming up DeckLink preroll from rendered system-memory frames.");
|
|
return false;
|
|
}
|
|
|
|
void VideoBackend::OutputCompletionWorkerMain()
|
|
{
|
|
for (;;)
|
|
{
|
|
VideoIOCompletion completion;
|
|
{
|
|
std::unique_lock<std::mutex> lock(mOutputCompletionMutex);
|
|
mOutputCompletionCondition.wait(lock, [this]() {
|
|
return mOutputCompletionWorkerStopping || !mPendingOutputCompletions.empty();
|
|
});
|
|
|
|
if (mPendingOutputCompletions.empty())
|
|
{
|
|
if (mOutputCompletionWorkerStopping)
|
|
{
|
|
mOutputCompletionWorkerRunning = false;
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
completion = mPendingOutputCompletions.front();
|
|
mPendingOutputCompletions.pop_front();
|
|
}
|
|
|
|
ProcessOutputFrameCompletion(completion);
|
|
}
|
|
}
|
|
|
|
void VideoBackend::OutputProducerWorkerMain()
|
|
{
|
|
for (;;)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
|
if (mOutputProducerWorkerStopping)
|
|
{
|
|
mOutputProducerWorkerRunning = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ScheduleReadyOutputFramesToTarget();
|
|
|
|
const RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
|
RecordReadyQueueDepthSample(metrics);
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
RenderCadenceDecision cadenceDecision = mRenderCadenceController.Tick(now);
|
|
if (cadenceDecision.action == RenderCadenceAction::Wait)
|
|
{
|
|
const auto waitDuration = (std::min)(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(cadenceDecision.waitDuration),
|
|
OutputProducerWakeInterval());
|
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
|
mOutputProducerCondition.wait_for(lock, waitDuration);
|
|
if (mOutputProducerWorkerStopping)
|
|
{
|
|
mOutputProducerWorkerRunning = false;
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
VideoIOCompletion completion;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
|
if (mOutputProducerWorkerStopping)
|
|
continue;
|
|
completion = mLastOutputProductionCompletion;
|
|
}
|
|
|
|
const std::size_t producedFrames = ProduceReadyOutputFrames(completion, 1);
|
|
if (producedFrames > 0)
|
|
{
|
|
mLastOutputProductionTime = std::chrono::steady_clock::now();
|
|
ScheduleReadyOutputFramesToTarget();
|
|
continue;
|
|
}
|
|
|
|
{
|
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
|
mOutputProducerCondition.wait_for(lock, OutputProducerWakeInterval());
|
|
if (mOutputProducerWorkerStopping)
|
|
{
|
|
mOutputProducerWorkerRunning = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::chrono::milliseconds VideoBackend::OutputProducerWakeInterval() const
|
|
{
|
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds;
|
|
if (frameBudgetMilliseconds <= 0.0)
|
|
return std::chrono::milliseconds(8);
|
|
|
|
const int intervalMilliseconds = (std::max)(1, static_cast<int>(std::floor(frameBudgetMilliseconds * 0.75)));
|
|
return std::chrono::milliseconds(intervalMilliseconds);
|
|
}
|
|
|
|
void VideoBackend::ProcessOutputFrameCompletion(const VideoIOCompletion& completion)
|
|
{
|
|
if (completion.outputFrameBuffer != nullptr)
|
|
mSystemOutputFramePool.ReleaseSlotByBuffer(completion.outputFrameBuffer);
|
|
RecordFramePacing(completion.result);
|
|
PublishOutputFrameCompleted(completion);
|
|
const RenderOutputQueueMetrics initialQueueMetrics = mReadyOutputQueue.GetMetrics();
|
|
RecordReadyQueueDepthSample(initialQueueMetrics);
|
|
const VideoPlayoutRecoveryDecision recoveryDecision = AccountForCompletionResult(completion.result, initialQueueMetrics.depth);
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
|
mLastLateStreak = recoveryDecision.lateStreak;
|
|
mLastDropStreak = recoveryDecision.dropStreak;
|
|
}
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
|
mLastOutputProductionCompletion = completion;
|
|
}
|
|
NotifyOutputProducer();
|
|
|
|
RecordBackendPlayoutHealth(completion.result, recoveryDecision);
|
|
RecordSystemMemoryPlayoutStats();
|
|
}
|
|
|
|
std::size_t VideoBackend::ScheduleReadyOutputFramesToTarget()
|
|
{
|
|
const std::size_t targetScheduledFrames = static_cast<std::size_t>(mPlayoutPolicy.targetPrerollFrames);
|
|
std::size_t scheduledFrames = 0;
|
|
for (;;)
|
|
{
|
|
const SystemOutputFramePoolMetrics poolMetrics = mSystemOutputFramePool.GetMetrics();
|
|
if (poolMetrics.scheduledCount >= targetScheduledFrames)
|
|
break;
|
|
if (!ScheduleReadyOutputFrame())
|
|
break;
|
|
++scheduledFrames;
|
|
}
|
|
return scheduledFrames;
|
|
}
|
|
|
|
void VideoBackend::RecordBackendPlayoutHealth(VideoIOCompletionResult result, const VideoPlayoutRecoveryDecision& recoveryDecision)
|
|
{
|
|
const RenderOutputQueueMetrics queueMetrics = mReadyOutputQueue.GetMetrics();
|
|
std::size_t minReadyQueueDepth = 0;
|
|
std::size_t maxReadyQueueDepth = 0;
|
|
uint64_t readyQueueZeroDepthCount = 0;
|
|
double outputRenderMilliseconds = 0.0;
|
|
double smoothedOutputRenderMilliseconds = 0.0;
|
|
double maxOutputRenderMilliseconds = 0.0;
|
|
double outputFrameAcquireMilliseconds = 0.0;
|
|
double outputFrameRenderRequestMilliseconds = 0.0;
|
|
double outputFrameEndAccessMilliseconds = 0.0;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
|
minReadyQueueDepth = mMinReadyQueueDepth;
|
|
maxReadyQueueDepth = mMaxReadyQueueDepth;
|
|
readyQueueZeroDepthCount = mReadyQueueZeroDepthCount;
|
|
outputRenderMilliseconds = mOutputRenderMilliseconds;
|
|
smoothedOutputRenderMilliseconds = mSmoothedOutputRenderMilliseconds;
|
|
maxOutputRenderMilliseconds = mMaxOutputRenderMilliseconds;
|
|
outputFrameAcquireMilliseconds = mOutputFrameAcquireMilliseconds;
|
|
outputFrameRenderRequestMilliseconds = mOutputFrameRenderRequestMilliseconds;
|
|
outputFrameEndAccessMilliseconds = mOutputFrameEndAccessMilliseconds;
|
|
}
|
|
|
|
mHealthTelemetry.TryRecordBackendPlayoutHealth(
|
|
VideoBackendLifecycle::StateName(mLifecycle.State()),
|
|
CompletionResultName(result),
|
|
queueMetrics.depth,
|
|
queueMetrics.capacity,
|
|
queueMetrics.pushedCount,
|
|
minReadyQueueDepth,
|
|
maxReadyQueueDepth,
|
|
readyQueueZeroDepthCount,
|
|
queueMetrics.poppedCount,
|
|
queueMetrics.droppedCount,
|
|
queueMetrics.underrunCount,
|
|
outputRenderMilliseconds,
|
|
smoothedOutputRenderMilliseconds,
|
|
maxOutputRenderMilliseconds,
|
|
outputFrameAcquireMilliseconds,
|
|
outputFrameRenderRequestMilliseconds,
|
|
outputFrameEndAccessMilliseconds,
|
|
recoveryDecision.completedFrameIndex,
|
|
recoveryDecision.scheduledFrameIndex,
|
|
recoveryDecision.scheduledLeadFrames,
|
|
recoveryDecision.measuredLagFrames,
|
|
recoveryDecision.catchUpFrames,
|
|
recoveryDecision.lateStreak,
|
|
recoveryDecision.dropStreak,
|
|
mLateFrameCount,
|
|
mDroppedFrameCount,
|
|
mFlushedFrameCount,
|
|
mLifecycle.State() == VideoBackendLifecycleState::Degraded,
|
|
StatusMessage());
|
|
}
|
|
|
|
std::size_t VideoBackend::ProduceReadyOutputFrames(const VideoIOCompletion& completion, std::size_t maxFrames)
|
|
{
|
|
if (maxFrames == 0)
|
|
return 0;
|
|
|
|
std::lock_guard<std::mutex> productionLock(mOutputProductionMutex);
|
|
RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
|
std::size_t producedFrames = 0;
|
|
while (producedFrames < maxFrames)
|
|
{
|
|
if (!RenderReadyOutputFrame(mVideoIODevice->State(), completion))
|
|
break;
|
|
++producedFrames;
|
|
metrics = mReadyOutputQueue.GetMetrics();
|
|
RecordReadyQueueDepthSample(metrics);
|
|
}
|
|
return producedFrames;
|
|
}
|
|
|
|
OutputProductionPressure VideoBackend::BuildOutputProductionPressure(const RenderOutputQueueMetrics& metrics) const
|
|
{
|
|
OutputProductionPressure pressure;
|
|
pressure.readyQueueDepth = metrics.depth;
|
|
pressure.readyQueueCapacity = metrics.capacity;
|
|
pressure.readyQueueUnderrunCount = metrics.underrunCount;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
|
pressure.lateStreak = mLastLateStreak;
|
|
pressure.dropStreak = mLastDropStreak;
|
|
}
|
|
return pressure;
|
|
}
|
|
|
|
bool VideoBackend::RenderReadyOutputFrame(const VideoIOState& state, const VideoIOCompletion& completion)
|
|
{
|
|
const auto renderStart = std::chrono::steady_clock::now();
|
|
OutputFrameSlot outputSlot;
|
|
VideoIOOutputFrame outputFrame;
|
|
const auto acquireStart = std::chrono::steady_clock::now();
|
|
if (!mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
|
{
|
|
if (!mReadyOutputQueue.DropOldestFrame() || !mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
|
return false;
|
|
}
|
|
outputFrame = outputSlot.frame;
|
|
const auto acquireEnd = std::chrono::steady_clock::now();
|
|
|
|
bool rendered = true;
|
|
const auto renderRequestStart = std::chrono::steady_clock::now();
|
|
if (mBridge)
|
|
rendered = mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
|
const auto renderRequestEnd = std::chrono::steady_clock::now();
|
|
|
|
const auto endAccessStart = std::chrono::steady_clock::now();
|
|
const bool publishedReady = mSystemOutputFramePool.PublishReadySlot(outputSlot);
|
|
const auto endAccessEnd = std::chrono::steady_clock::now();
|
|
const double acquireMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(acquireEnd - acquireStart).count();
|
|
const double renderRequestMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderRequestEnd - renderRequestStart).count();
|
|
const double endAccessMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(endAccessEnd - endAccessStart).count();
|
|
|
|
if (!rendered)
|
|
{
|
|
mSystemOutputFramePool.ReleaseSlot(outputSlot);
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output frame render request failed; skipping schedule for this frame.");
|
|
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
|
std::chrono::steady_clock::now() - renderStart).count();
|
|
RecordOutputRenderDuration(renderMilliseconds, acquireMilliseconds, renderRequestMilliseconds, endAccessMilliseconds);
|
|
return false;
|
|
}
|
|
|
|
if (!publishedReady)
|
|
{
|
|
mSystemOutputFramePool.ReleaseSlot(outputSlot);
|
|
return false;
|
|
}
|
|
|
|
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
|
std::chrono::steady_clock::now() - renderStart).count();
|
|
RecordOutputRenderDuration(renderMilliseconds, acquireMilliseconds, renderRequestMilliseconds, endAccessMilliseconds);
|
|
|
|
RenderOutputFrame readyFrame;
|
|
readyFrame.frame = outputFrame;
|
|
readyFrame.frameIndex = ++mNextReadyOutputFrameIndex;
|
|
readyFrame.releaseFrame = [this](VideoIOOutputFrame& frame) {
|
|
mSystemOutputFramePool.ReleaseSlotByBuffer(frame.bytes);
|
|
};
|
|
const bool pushed = mReadyOutputQueue.Push(readyFrame);
|
|
if (!pushed)
|
|
mSystemOutputFramePool.ReleaseSlot(outputSlot);
|
|
RecordSystemMemoryPlayoutStats();
|
|
return pushed;
|
|
}
|
|
|
|
bool VideoBackend::ScheduleReadyOutputFrame()
|
|
{
|
|
std::lock_guard<std::mutex> schedulingLock(mOutputSchedulingMutex);
|
|
RenderOutputFrame readyFrame;
|
|
if (!mReadyOutputQueue.TryPop(readyFrame))
|
|
return false;
|
|
RecordReadyQueueDepthSample(mReadyOutputQueue.GetMetrics());
|
|
|
|
if (!mSystemOutputFramePool.MarkScheduledByBuffer(readyFrame.frame.bytes))
|
|
{
|
|
if (readyFrame.releaseFrame)
|
|
readyFrame.releaseFrame(readyFrame.frame);
|
|
return false;
|
|
}
|
|
|
|
if (!ScheduleOutputFrame(readyFrame.frame))
|
|
{
|
|
RecordDeckLinkBufferTelemetry();
|
|
mSystemOutputFramePool.ReleaseSlotByBuffer(readyFrame.frame.bytes);
|
|
return false;
|
|
}
|
|
|
|
RecordDeckLinkBufferTelemetry();
|
|
PublishOutputFrameScheduled(readyFrame.frame);
|
|
RecordSystemMemoryPlayoutStats();
|
|
return true;
|
|
}
|
|
|
|
bool VideoBackend::ScheduleBlackUnderrunFrame()
|
|
{
|
|
VideoIOOutputFrame outputFrame;
|
|
if (!BeginOutputFrame(outputFrame))
|
|
{
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: no output frame was available for fallback scheduling.");
|
|
return false;
|
|
}
|
|
|
|
if (outputFrame.bytes != nullptr && outputFrame.rowBytes > 0 && outputFrame.height > 0)
|
|
std::memset(outputFrame.bytes, 0, static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height);
|
|
EndOutputFrame(outputFrame);
|
|
|
|
if (!ScheduleOutputFrame(outputFrame))
|
|
{
|
|
RecordDeckLinkBufferTelemetry();
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: black fallback frame scheduling failed.");
|
|
return false;
|
|
}
|
|
|
|
RecordDeckLinkBufferTelemetry();
|
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: scheduled black fallback frame.");
|
|
PublishOutputFrameScheduled(outputFrame);
|
|
return true;
|
|
}
|
|
|
|
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::RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
|
if (!mHasReadyQueueDepthBaseline)
|
|
{
|
|
mHasReadyQueueDepthBaseline = true;
|
|
mMinReadyQueueDepth = metrics.depth;
|
|
mMaxReadyQueueDepth = metrics.depth;
|
|
}
|
|
else
|
|
{
|
|
mMinReadyQueueDepth = (std::min)(mMinReadyQueueDepth, metrics.depth);
|
|
mMaxReadyQueueDepth = (std::max)(mMaxReadyQueueDepth, metrics.depth);
|
|
}
|
|
|
|
if (metrics.depth == 0)
|
|
++mReadyQueueZeroDepthCount;
|
|
}
|
|
|
|
void VideoBackend::RecordDeckLinkBufferTelemetry()
|
|
{
|
|
if (!mVideoIODevice)
|
|
return;
|
|
|
|
const VideoIOState& state = mVideoIODevice->State();
|
|
mHealthTelemetry.TryRecordDeckLinkBufferTelemetry(
|
|
state.actualDeckLinkBufferedFramesAvailable,
|
|
state.actualDeckLinkBufferedFrames,
|
|
static_cast<std::size_t>(mPlayoutPolicy.targetPrerollFrames),
|
|
state.deckLinkScheduleCallMilliseconds,
|
|
state.deckLinkScheduleFailureCount);
|
|
}
|
|
|
|
void VideoBackend::RecordSystemMemoryPlayoutStats()
|
|
{
|
|
const SystemOutputFramePoolMetrics poolMetrics = mSystemOutputFramePool.GetMetrics();
|
|
const RenderOutputQueueMetrics queueMetrics = mReadyOutputQueue.GetMetrics();
|
|
RecordDeckLinkBufferTelemetry();
|
|
mHealthTelemetry.TryRecordSystemMemoryPlayoutStats(
|
|
poolMetrics.freeCount,
|
|
poolMetrics.readyCount,
|
|
poolMetrics.scheduledCount,
|
|
poolMetrics.readyUnderrunCount,
|
|
0,
|
|
queueMetrics.droppedCount,
|
|
0.0,
|
|
0.0);
|
|
}
|
|
|
|
void VideoBackend::RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
|
mOutputRenderMilliseconds = (std::max)(renderMilliseconds, 0.0);
|
|
if (mSmoothedOutputRenderMilliseconds <= 0.0)
|
|
mSmoothedOutputRenderMilliseconds = mOutputRenderMilliseconds;
|
|
else
|
|
mSmoothedOutputRenderMilliseconds = mSmoothedOutputRenderMilliseconds * 0.9 + mOutputRenderMilliseconds * 0.1;
|
|
mMaxOutputRenderMilliseconds = (std::max)(mMaxOutputRenderMilliseconds, mOutputRenderMilliseconds);
|
|
mOutputFrameAcquireMilliseconds = (std::max)(acquireMilliseconds, 0.0);
|
|
mOutputFrameRenderRequestMilliseconds = (std::max)(renderRequestMilliseconds, 0.0);
|
|
mOutputFrameEndAccessMilliseconds = (std::max)(endAccessMilliseconds, 0.0);
|
|
|
|
PublishTimingSample("VideoBackend", "outputRender", mOutputRenderMilliseconds, "ms");
|
|
PublishTimingSample("VideoBackend", "smoothedOutputRender", mSmoothedOutputRenderMilliseconds, "ms");
|
|
}
|
|
|
|
bool VideoBackend::ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message)
|
|
{
|
|
const VideoBackendLifecycleTransition transition = mLifecycle.TransitionTo(state, message);
|
|
if (!transition.accepted)
|
|
{
|
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), transition.errorMessage);
|
|
return false;
|
|
}
|
|
|
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), message);
|
|
return true;
|
|
}
|
|
|
|
bool VideoBackend::ApplyLifecycleFailure(const std::string& message)
|
|
{
|
|
const VideoBackendLifecycleTransition transition = mLifecycle.Fail(message);
|
|
if (!transition.accepted)
|
|
{
|
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), transition.errorMessage);
|
|
return false;
|
|
}
|
|
|
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), message);
|
|
return true;
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
bool VideoBackend::IsEnvironmentFlagEnabled(const char* name)
|
|
{
|
|
if (name == nullptr || name[0] == '\0')
|
|
return false;
|
|
|
|
char* value = nullptr;
|
|
std::size_t valueSize = 0;
|
|
if (_dupenv_s(&value, &valueSize, name) != 0 || value == nullptr)
|
|
return false;
|
|
|
|
const std::string flag(value);
|
|
std::free(value);
|
|
return flag == "1" || flag == "true" || flag == "TRUE" || flag == "yes" || flag == "on";
|
|
}
|