934 lines
29 KiB
C++
934 lines
29 KiB
C++
#include "DeckLinkSession.h"
|
|
|
|
#include <atlbase.h>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <new>
|
|
#include <sstream>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
|
|
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
|
|
|
|
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
|
{
|
|
public:
|
|
SystemMemoryDeckLinkVideoBuffer(void* bytes, unsigned long long sizeBytes) :
|
|
mBytes(bytes),
|
|
mSizeBytes(sizeBytes),
|
|
mRefCount(1)
|
|
{
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override
|
|
{
|
|
if (ppv == nullptr)
|
|
return E_POINTER;
|
|
if (iid == IID_IUnknown || iid == IID_IDeckLinkVideoBuffer)
|
|
{
|
|
*ppv = static_cast<IDeckLinkVideoBuffer*>(this);
|
|
AddRef();
|
|
return S_OK;
|
|
}
|
|
*ppv = nullptr;
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
ULONG STDMETHODCALLTYPE AddRef() override
|
|
{
|
|
return ++mRefCount;
|
|
}
|
|
|
|
ULONG STDMETHODCALLTYPE Release() override
|
|
{
|
|
const ULONG refCount = --mRefCount;
|
|
if (refCount == 0)
|
|
delete this;
|
|
return refCount;
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override
|
|
{
|
|
if (buffer == nullptr)
|
|
return E_POINTER;
|
|
*buffer = mBytes;
|
|
return mBytes != nullptr ? S_OK : E_FAIL;
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE GetSize(unsigned long long* bufferSize) override
|
|
{
|
|
if (bufferSize == nullptr)
|
|
return E_POINTER;
|
|
*bufferSize = mSizeBytes;
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags) override
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags) override
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
private:
|
|
void* mBytes = nullptr;
|
|
unsigned long long mSizeBytes = 0;
|
|
std::atomic<ULONG> mRefCount;
|
|
};
|
|
|
|
std::string BstrToUtf8(BSTR value)
|
|
{
|
|
if (value == nullptr)
|
|
return std::string();
|
|
|
|
const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
|
|
if (requiredBytes <= 1)
|
|
return std::string();
|
|
|
|
std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
|
|
if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
|
|
return std::string();
|
|
|
|
return std::string(utf8Name.data());
|
|
}
|
|
|
|
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
|
{
|
|
if (input == nullptr)
|
|
return false;
|
|
|
|
BOOL supported = FALSE;
|
|
BMDDisplayMode actualMode = bmdModeUnknown;
|
|
const HRESULT result = input->DoesSupportVideoMode(
|
|
bmdVideoConnectionUnspecified,
|
|
displayMode,
|
|
pixelFormat,
|
|
bmdNoVideoInputConversion,
|
|
bmdSupportedVideoModeDefault,
|
|
&actualMode,
|
|
&supported);
|
|
return result == S_OK && supported != FALSE;
|
|
}
|
|
|
|
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
|
|
{
|
|
if (output == nullptr)
|
|
return false;
|
|
|
|
BOOL supported = FALSE;
|
|
BMDDisplayMode actualMode = bmdModeUnknown;
|
|
const HRESULT result = output->DoesSupportVideoMode(
|
|
bmdVideoConnectionUnspecified,
|
|
displayMode,
|
|
pixelFormat,
|
|
bmdNoVideoOutputConversion,
|
|
bmdSupportedVideoModeDefault,
|
|
&actualMode,
|
|
&supported);
|
|
return result == S_OK && supported != FALSE;
|
|
}
|
|
}
|
|
|
|
DeckLinkSession::~DeckLinkSession()
|
|
{
|
|
ReleaseResources();
|
|
}
|
|
|
|
void DeckLinkSession::ReleaseResources()
|
|
{
|
|
if (input != nullptr)
|
|
input->SetCallback(nullptr);
|
|
captureDelegate.Release();
|
|
input.Release();
|
|
|
|
if (output != nullptr)
|
|
output->SetScheduledFrameCompletionCallback(nullptr);
|
|
|
|
if (keyer != nullptr)
|
|
{
|
|
keyer->Disable();
|
|
mState.externalKeyingActive = false;
|
|
}
|
|
keyer.Release();
|
|
|
|
playoutDelegate.Release();
|
|
outputVideoFrameQueue.clear();
|
|
output.Release();
|
|
}
|
|
|
|
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
|
{
|
|
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
|
CComPtr<IDeckLinkDisplayMode> inputMode;
|
|
CComPtr<IDeckLinkDisplayMode> outputMode;
|
|
|
|
mState.inputDisplayModeName = videoModes.input.displayName;
|
|
mState.outputDisplayModeName = videoModes.output.displayName;
|
|
|
|
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
|
if (FAILED(result))
|
|
{
|
|
error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
|
|
return false;
|
|
}
|
|
|
|
CComPtr<IDeckLink> deckLink;
|
|
while (deckLinkIterator->Next(&deckLink) == S_OK)
|
|
{
|
|
int64_t duplexMode;
|
|
bool deviceSupportsInternalKeying = false;
|
|
bool deviceSupportsExternalKeying = false;
|
|
std::string modelName;
|
|
CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;
|
|
|
|
if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
|
|
{
|
|
printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
|
|
deckLink.Release();
|
|
continue;
|
|
}
|
|
|
|
result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
|
|
BOOL attributeFlag = FALSE;
|
|
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
|
|
deviceSupportsInternalKeying = (attributeFlag != FALSE);
|
|
attributeFlag = FALSE;
|
|
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
|
|
deviceSupportsExternalKeying = (attributeFlag != FALSE);
|
|
CComBSTR modelNameBstr;
|
|
if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
|
|
modelName = BstrToUtf8(modelNameBstr);
|
|
|
|
if (result != S_OK || duplexMode == bmdDuplexInactive)
|
|
{
|
|
deckLink.Release();
|
|
continue;
|
|
}
|
|
|
|
bool inputUsed = false;
|
|
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
|
|
inputUsed = true;
|
|
|
|
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
|
|
{
|
|
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
|
|
output.Release();
|
|
else
|
|
{
|
|
mState.outputModelName = modelName;
|
|
mState.supportsInternalKeying = deviceSupportsInternalKeying;
|
|
mState.supportsExternalKeying = deviceSupportsExternalKeying;
|
|
}
|
|
}
|
|
|
|
deckLink.Release();
|
|
|
|
if (output && input)
|
|
break;
|
|
}
|
|
|
|
if (!output)
|
|
{
|
|
error = "Expected an Output DeckLink device";
|
|
ReleaseResources();
|
|
return false;
|
|
}
|
|
|
|
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
|
|
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
|
|
{
|
|
error = "Cannot get input Display Mode Iterator.";
|
|
ReleaseResources();
|
|
return false;
|
|
}
|
|
|
|
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
|
|
{
|
|
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
|
|
ReleaseResources();
|
|
return false;
|
|
}
|
|
inputDisplayModeIterator.Release();
|
|
|
|
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
|
|
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
|
|
{
|
|
error = "Cannot get output Display Mode Iterator.";
|
|
ReleaseResources();
|
|
return false;
|
|
}
|
|
|
|
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
|
|
{
|
|
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
|
|
ReleaseResources();
|
|
return false;
|
|
}
|
|
|
|
mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
|
|
mState.inputFrameSize = inputMode
|
|
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
|
|
: mState.outputFrameSize;
|
|
if (!input)
|
|
mState.inputDisplayModeName = "No input - black frame";
|
|
BMDTimeValue frameDuration = 0;
|
|
BMDTimeScale frameTimescale = 0;
|
|
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
|
mScheduler.Configure(frameDuration, frameTimescale, mPlayoutPolicy);
|
|
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
|
|
|
|
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
|
mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
|
|
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
|
mState.outputPackTextureWidth = mState.outputFrameSize.width;
|
|
mState.hasInputDevice = input != nullptr;
|
|
mState.hasInputSource = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
|
{
|
|
if (!output)
|
|
{
|
|
error = "Expected an Output DeckLink device";
|
|
return false;
|
|
}
|
|
|
|
mState.formatStatusMessage.clear();
|
|
|
|
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
|
|
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
|
|
if (input != nullptr && !inputTenBitSupported)
|
|
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
|
|
|
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
|
|
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, 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. ";
|
|
|
|
int deckLinkOutputRowBytes = 0;
|
|
if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
|
|
{
|
|
error = "DeckLink output setup failed while calculating output row bytes.";
|
|
return false;
|
|
}
|
|
mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
|
|
mState.outputPackTextureWidth = OutputIsTenBit()
|
|
? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
|
|
: mState.outputFrameSize.width;
|
|
|
|
if (InputIsTenBit())
|
|
{
|
|
int deckLinkInputRowBytes = 0;
|
|
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
|
|
mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
|
|
else
|
|
mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
|
|
}
|
|
else
|
|
{
|
|
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
|
}
|
|
mState.captureTextureWidth = InputIsTenBit()
|
|
? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
|
|
: mState.inputFrameSize.width / 2u;
|
|
|
|
std::ostringstream status;
|
|
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
|
|
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
|
|
if (!mState.formatStatusMessage.empty())
|
|
status << " " << mState.formatStatusMessage;
|
|
mState.formatStatusMessage = status.str();
|
|
return true;
|
|
}
|
|
|
|
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
|
|
{
|
|
mInputFrameCallback = std::move(callback);
|
|
|
|
if (!input)
|
|
{
|
|
mState.hasInputSource = false;
|
|
mState.inputDisplayModeName = "No input - black frame";
|
|
return true;
|
|
}
|
|
|
|
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
|
|
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
|
{
|
|
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
|
|
{
|
|
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
|
|
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
|
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
|
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
|
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
|
|
{
|
|
std::ostringstream status;
|
|
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
|
|
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
|
|
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
|
|
mState.formatStatusMessage = status.str();
|
|
goto input_enabled;
|
|
}
|
|
}
|
|
|
|
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
|
|
input.Release();
|
|
mState.hasInputDevice = false;
|
|
mState.hasInputSource = false;
|
|
mState.inputDisplayModeName = "No input - black frame";
|
|
return true;
|
|
}
|
|
|
|
input_enabled:
|
|
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
|
|
if (captureDelegate == nullptr)
|
|
{
|
|
error = "DeckLink input setup failed while creating the capture callback.";
|
|
return false;
|
|
}
|
|
if (input->SetCallback(captureDelegate) != S_OK)
|
|
{
|
|
error = "DeckLink input setup failed while installing the capture callback.";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
|
{
|
|
mOutputFrameCallback = std::move(callback);
|
|
|
|
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
|
|
{
|
|
error = "DeckLink output setup failed while enabling video output.";
|
|
return false;
|
|
}
|
|
|
|
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
|
|
mState.keyerInterfaceAvailable = true;
|
|
|
|
if (externalKeyingEnabled)
|
|
{
|
|
if (!mState.supportsExternalKeying)
|
|
{
|
|
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
|
}
|
|
else if (!mState.keyerInterfaceAvailable)
|
|
{
|
|
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
|
|
}
|
|
else if (keyer->Enable(TRUE) != S_OK || keyer->SetLevel(255) != S_OK)
|
|
{
|
|
mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
|
|
}
|
|
else
|
|
{
|
|
mState.externalKeyingActive = true;
|
|
mState.statusMessage = "External keying is active on the selected DeckLink output.";
|
|
}
|
|
}
|
|
else if (mState.supportsExternalKeying)
|
|
{
|
|
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
|
}
|
|
|
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
|
mPlayoutPolicy = policy;
|
|
for (unsigned i = 0; i < policy.outputFramePoolSize; i++)
|
|
{
|
|
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
|
|
|
|
const BMDPixelFormat deckLinkOutputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat);
|
|
if (output->CreateVideoFrame(mState.outputFrameSize.width, mState.outputFrameSize.height, mState.outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
|
|
{
|
|
error = "DeckLink output setup failed while creating an output video frame.";
|
|
return false;
|
|
}
|
|
|
|
outputVideoFrameQueue.push_back(outputFrame);
|
|
}
|
|
|
|
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
|
|
if (playoutDelegate == nullptr)
|
|
{
|
|
error = "DeckLink output setup failed while creating the playout callback.";
|
|
return false;
|
|
}
|
|
|
|
if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
|
|
{
|
|
error = "DeckLink output setup failed while installing the scheduled-frame callback.";
|
|
return false;
|
|
}
|
|
|
|
if (!mState.formatStatusMessage.empty())
|
|
mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;
|
|
|
|
return true;
|
|
}
|
|
|
|
double DeckLinkSession::FrameBudgetMilliseconds() const
|
|
{
|
|
return mScheduler.FrameBudgetMilliseconds();
|
|
}
|
|
|
|
bool DeckLinkSession::AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame)
|
|
{
|
|
if (outputVideoFrameQueue.empty())
|
|
return false;
|
|
|
|
outputVideoFrame = outputVideoFrameQueue.front();
|
|
outputVideoFrameQueue.pop_front();
|
|
return outputVideoFrame != nullptr;
|
|
}
|
|
|
|
bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame)
|
|
{
|
|
if (outputVideoFrame == nullptr)
|
|
return false;
|
|
|
|
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
|
|
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
|
return false;
|
|
|
|
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
|
|
return false;
|
|
|
|
void* pFrame = nullptr;
|
|
outputVideoFrameBuffer->GetBytes(&pFrame);
|
|
|
|
frame.bytes = pFrame;
|
|
frame.rowBytes = outputVideoFrame->GetRowBytes();
|
|
frame.width = mState.outputFrameSize.width;
|
|
frame.height = mState.outputFrameSize.height;
|
|
frame.pixelFormat = mState.outputPixelFormat;
|
|
outputVideoFrame->AddRef();
|
|
frame.nativeFrame = outputVideoFrame;
|
|
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
|
|
return true;
|
|
}
|
|
|
|
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
|
{
|
|
if (outputVideoFrame == nullptr || output == nullptr)
|
|
{
|
|
++mState.deckLinkScheduleFailureCount;
|
|
return false;
|
|
}
|
|
|
|
if (mScheduleRealignmentPending)
|
|
{
|
|
RealignScheduleCursorToPlayback();
|
|
mScheduleRealignmentPending = false;
|
|
}
|
|
|
|
UpdateScheduleLeadTelemetry();
|
|
MaybeRealignScheduleCursorForLowLead();
|
|
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
|
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
|
const auto scheduleEnd = std::chrono::steady_clock::now();
|
|
mState.deckLinkScheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
|
|
if (result != S_OK)
|
|
++mState.deckLinkScheduleFailureCount;
|
|
RefreshBufferedVideoFrameCount();
|
|
return result == S_OK;
|
|
}
|
|
|
|
void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
|
{
|
|
if (output == nullptr)
|
|
{
|
|
mState.deckLinkScheduleLeadAvailable = false;
|
|
return;
|
|
}
|
|
|
|
BMDTimeValue streamTime = 0;
|
|
double playbackSpeed = 0.0;
|
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
|
{
|
|
mState.deckLinkScheduleLeadAvailable = false;
|
|
return;
|
|
}
|
|
|
|
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
|
|
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
|
|
: 0;
|
|
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
|
|
mState.deckLinkScheduleLeadAvailable = true;
|
|
mState.deckLinkPlaybackStreamTime = streamTime;
|
|
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
|
|
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
|
|
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
|
}
|
|
|
|
void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
|
|
{
|
|
if (!mState.deckLinkScheduleLeadAvailable)
|
|
return;
|
|
|
|
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
|
{
|
|
mProactiveScheduleRealignmentArmed = true;
|
|
return;
|
|
}
|
|
|
|
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
|
return;
|
|
|
|
RealignScheduleCursorToPlayback();
|
|
mProactiveScheduleRealignmentArmed = false;
|
|
}
|
|
|
|
void DeckLinkSession::RealignScheduleCursorToPlayback()
|
|
{
|
|
if (output == nullptr)
|
|
return;
|
|
|
|
BMDTimeValue streamTime = 0;
|
|
double playbackSpeed = 0.0;
|
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
|
return;
|
|
|
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
|
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
|
|
++mState.deckLinkScheduleRealignmentCount;
|
|
UpdateScheduleLeadTelemetry();
|
|
}
|
|
|
|
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
|
{
|
|
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
|
return false;
|
|
|
|
CComPtr<IDeckLinkVideoBuffer> videoBuffer;
|
|
videoBuffer.Attach(new (std::nothrow) SystemMemoryDeckLinkVideoBuffer(
|
|
frame.bytes,
|
|
static_cast<unsigned long long>(frame.rowBytes) * static_cast<unsigned long long>(frame.height)));
|
|
if (videoBuffer == nullptr)
|
|
return false;
|
|
|
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
|
const BMDPixelFormat pixelFormat = DeckLinkPixelFormatForVideoIO(frame.pixelFormat);
|
|
if (output->CreateVideoFrameWithBuffer(
|
|
frame.width,
|
|
frame.height,
|
|
frame.rowBytes,
|
|
pixelFormat,
|
|
bmdFrameFlagFlipVertical,
|
|
videoBuffer,
|
|
&outputVideoFrame) != S_OK)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
IDeckLinkVideoFrame* scheduledFrame = outputVideoFrame;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
|
|
mScheduledSystemFrameBuffers[scheduledFrame] = frame.bytes;
|
|
}
|
|
|
|
if (ScheduleFrame(outputVideoFrame))
|
|
return true;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
|
|
mScheduledSystemFrameBuffers.erase(scheduledFrame);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
|
{
|
|
if (outputVideoFrame == nullptr)
|
|
return false;
|
|
|
|
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
|
|
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
|
|
return false;
|
|
|
|
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
|
|
return false;
|
|
|
|
void* pFrame = nullptr;
|
|
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
|
|
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
|
|
|
|
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
|
return ScheduleFrame(outputVideoFrame);
|
|
}
|
|
|
|
void DeckLinkSession::RefreshBufferedVideoFrameCount()
|
|
{
|
|
if (output == nullptr)
|
|
{
|
|
mState.actualDeckLinkBufferedFramesAvailable = false;
|
|
return;
|
|
}
|
|
|
|
unsigned int bufferedFrameCount = 0;
|
|
if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK)
|
|
{
|
|
mState.actualDeckLinkBufferedFrames = bufferedFrameCount;
|
|
mState.actualDeckLinkBufferedFramesAvailable = true;
|
|
}
|
|
else
|
|
{
|
|
mState.actualDeckLinkBufferedFramesAvailable = false;
|
|
}
|
|
}
|
|
|
|
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
|
|
{
|
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
|
return AcquireNextOutputVideoFrame(outputVideoFrame) && PopulateOutputFrame(outputVideoFrame, frame);
|
|
}
|
|
|
|
void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
|
|
{
|
|
IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast<IDeckLinkVideoBuffer*>(frame.nativeBuffer);
|
|
if (outputVideoFrameBuffer != nullptr)
|
|
{
|
|
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
|
|
outputVideoFrameBuffer->Release();
|
|
}
|
|
frame.nativeBuffer = nullptr;
|
|
frame.bytes = nullptr;
|
|
}
|
|
|
|
VideoPlayoutRecoveryDecision DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth)
|
|
{
|
|
return mScheduler.AccountForCompletionResult(completionResult, readyQueueDepth);
|
|
}
|
|
|
|
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
|
|
{
|
|
if (frame.nativeFrame == nullptr)
|
|
return ScheduleSystemMemoryFrame(frame);
|
|
|
|
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
|
|
const bool scheduled = ScheduleFrame(outputVideoFrame);
|
|
if (outputVideoFrame != nullptr)
|
|
outputVideoFrame->Release();
|
|
return scheduled;
|
|
}
|
|
|
|
bool DeckLinkSession::PrepareOutputSchedule()
|
|
{
|
|
mScheduler.Reset();
|
|
RefreshBufferedVideoFrameCount();
|
|
return output != nullptr;
|
|
}
|
|
|
|
bool DeckLinkSession::StartInputStreams()
|
|
{
|
|
if (!input)
|
|
return true;
|
|
|
|
if (input->StartStreams() != S_OK)
|
|
{
|
|
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DeckLinkSession::StartScheduledPlayback()
|
|
{
|
|
if (!output)
|
|
{
|
|
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
|
|
if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
|
|
{
|
|
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
RefreshBufferedVideoFrameCount();
|
|
return true;
|
|
}
|
|
|
|
bool DeckLinkSession::Start()
|
|
{
|
|
if (!output)
|
|
{
|
|
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
if (outputVideoFrameQueue.empty())
|
|
{
|
|
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
|
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
|
mPlayoutPolicy = policy;
|
|
if (!PrepareOutputSchedule())
|
|
return false;
|
|
|
|
for (unsigned i = 0; i < policy.targetPrerollFrames; i++)
|
|
{
|
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
|
if (!AcquireNextOutputVideoFrame(outputVideoFrame))
|
|
{
|
|
MessageBoxA(NULL, "Could not acquire a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
if (!ScheduleBlackFrame(outputVideoFrame))
|
|
{
|
|
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return StartInputStreams() && StartScheduledPlayback();
|
|
}
|
|
|
|
bool DeckLinkSession::Stop()
|
|
{
|
|
if (keyer != nullptr)
|
|
{
|
|
keyer->Disable();
|
|
mState.externalKeyingActive = false;
|
|
}
|
|
|
|
if (input)
|
|
{
|
|
input->StopStreams();
|
|
input->DisableVideoInput();
|
|
}
|
|
|
|
if (output)
|
|
{
|
|
output->StopScheduledPlayback(0, NULL, 0);
|
|
output->DisableVideoOutput();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
|
|
{
|
|
mState.hasInputSource = !hasNoInputSource;
|
|
if (hasNoInputSource || mInputFrameCallback == nullptr)
|
|
{
|
|
VideoIOFrame frame;
|
|
frame.width = mState.inputFrameSize.width;
|
|
frame.height = mState.inputFrameSize.height;
|
|
frame.pixelFormat = mState.inputPixelFormat;
|
|
frame.hasNoInputSource = hasNoInputSource;
|
|
if (mInputFrameCallback)
|
|
mInputFrameCallback(frame);
|
|
return;
|
|
}
|
|
|
|
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
|
|
void* videoPixels = nullptr;
|
|
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
|
|
return;
|
|
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
|
return;
|
|
|
|
inputFrameBuffer->GetBytes(&videoPixels);
|
|
|
|
VideoIOFrame frame;
|
|
frame.bytes = videoPixels;
|
|
frame.rowBytes = inputFrame->GetRowBytes();
|
|
frame.width = static_cast<unsigned>(inputFrame->GetWidth());
|
|
frame.height = static_cast<unsigned>(inputFrame->GetHeight());
|
|
frame.pixelFormat = mState.inputPixelFormat;
|
|
frame.hasNoInputSource = hasNoInputSource;
|
|
mInputFrameCallback(frame);
|
|
|
|
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
|
}
|
|
|
|
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
|
{
|
|
RefreshBufferedVideoFrameCount();
|
|
|
|
void* completedSystemBuffer = nullptr;
|
|
if (completedFrame != nullptr)
|
|
{
|
|
bool externalSystemFrame = false;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
|
|
auto externalFrame = mScheduledSystemFrameBuffers.find(completedFrame);
|
|
if (externalFrame != mScheduledSystemFrameBuffers.end())
|
|
{
|
|
completedSystemBuffer = externalFrame->second;
|
|
mScheduledSystemFrameBuffers.erase(externalFrame);
|
|
externalSystemFrame = true;
|
|
}
|
|
}
|
|
|
|
if (!externalSystemFrame)
|
|
{
|
|
CComPtr<IDeckLinkMutableVideoFrame> reusableFrame;
|
|
if (completedFrame->QueryInterface(IID_IDeckLinkMutableVideoFrame, reinterpret_cast<void**>(&reusableFrame)) == S_OK &&
|
|
reusableFrame != nullptr)
|
|
{
|
|
outputVideoFrameQueue.push_back(reusableFrame);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!mOutputFrameCallback)
|
|
return;
|
|
|
|
VideoIOCompletion completion;
|
|
completion.result = TranslateCompletionResult(completionResult);
|
|
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
|
|
{
|
|
if (mScheduleRealignmentArmed)
|
|
{
|
|
mScheduleRealignmentPending = true;
|
|
mScheduleRealignmentArmed = false;
|
|
}
|
|
}
|
|
else if (completion.result == VideoIOCompletionResult::Completed)
|
|
{
|
|
mScheduleRealignmentArmed = true;
|
|
}
|
|
completion.outputFrameBuffer = completedSystemBuffer;
|
|
mOutputFrameCallback(completion);
|
|
}
|
|
|
|
VideoIOCompletionResult DeckLinkSession::TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult)
|
|
{
|
|
switch (completionResult)
|
|
{
|
|
case bmdOutputFrameDisplayedLate:
|
|
return VideoIOCompletionResult::DisplayedLate;
|
|
case bmdOutputFrameDropped:
|
|
return VideoIOCompletionResult::Dropped;
|
|
case bmdOutputFrameFlushed:
|
|
return VideoIOCompletionResult::Flushed;
|
|
case bmdOutputFrameCompleted:
|
|
return VideoIOCompletionResult::Completed;
|
|
default:
|
|
return VideoIOCompletionResult::Unknown;
|
|
}
|
|
}
|