#include "DeckLinkSession.h" #include #include #include #include #include #include #include #include #include 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(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 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 utf8Name(static_cast(requiredBytes), '\0'); if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0) return std::string(); return std::string(utf8Name.data()); } 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; } bool RenderReadbackSupportsOutputFormat(VideoIOPixelFormat pixelFormat) { return pixelFormat == VideoIOPixelFormat::Bgra8 || pixelFormat == VideoIOPixelFormat::Uyvy8; } } DeckLinkSession::~DeckLinkSession() { ReleaseResources(); } void DeckLinkSession::ReleaseResources() { 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 deckLinkIterator; CComPtr outputMode; BMDDisplayMode outputDisplayMode = bmdModeUnknown; if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode)) { error = "Cannot map configured output mode to DeckLink BMDDisplayMode: " + videoModes.output.displayName; return false; } mState.outputDisplayModeName = videoModes.output.displayName; mState.inputDisplayModeName = "No input - output session"; HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast(&deckLinkIterator)); if (FAILED(result)) { error = "Please install the Blackmagic DeckLink drivers to use the features of this application."; return false; } CComPtr deckLink; while (deckLinkIterator->Next(&deckLink) == S_OK) { int64_t duplexMode; bool deviceSupportsInternalKeying = false; bool deviceSupportsExternalKeying = false; std::string modelName; CComPtr 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; } if (!output) { 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) break; } if (!output) { error = "Expected an Output DeckLink device"; ReleaseResources(); return false; } CComPtr outputDisplayModeIterator; if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK) { error = "Cannot get output Display Mode Iterator."; ReleaseResources(); return false; } if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, outputDisplayMode, &outputMode)) { error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName; ReleaseResources(); return false; } mState.outputFrameSize = { static_cast(outputMode->GetWidth()), static_cast(outputMode->GetHeight()) }; mState.inputFrameSize = mState.outputFrameSize; 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 = false; mState.hasInputSource = false; return true; } bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, VideoIOPixelFormat systemFramePixelFormat, bool outputAlphaRequired, std::string& error) { if (!output) { error = "Expected an Output DeckLink device"; return false; } mState.formatStatusMessage.clear(); BMDDisplayMode outputDisplayMode = bmdModeUnknown; if (!DeckLinkDisplayModeForVideoFormat(videoModes.output, outputDisplayMode)) { error = "DeckLink format selection failed while mapping the configured output mode."; return false; } mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8; if (!RenderReadbackSupportsOutputFormat(systemFramePixelFormat)) { error = "DeckLink output requested " + std::string(VideoIOPixelFormatName(systemFramePixelFormat)) + ", but render readback currently supports only BGRA8 and UYVY8 system frames."; return false; } if (outputAlphaRequired && systemFramePixelFormat != VideoIOPixelFormat::Bgra8) { error = "DeckLink alpha output requires BGRA8 system frames until a YUVA render packer exists."; return false; } const BMDPixelFormat requestedOutputPixelFormat = DeckLinkPixelFormatForVideoIO(systemFramePixelFormat); if (!OutputSupportsFormat(output, outputDisplayMode, requestedOutputPixelFormat)) { error = "DeckLink output does not report support for " + std::string(VideoIOPixelFormatName(systemFramePixelFormat)) + " in the configured display mode."; return false; } mState.outputPixelFormat = systemFramePixelFormat; if (outputAlphaRequired) mState.formatStatusMessage += "Output alpha requires BGRA8 system frames. "; 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(deckLinkOutputRowBytes); if (OutputIsTenBit()) mState.outputPackTextureWidth = PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes); else if (mState.outputPixelFormat == VideoIOPixelFormat::Uyvy8) mState.outputPackTextureWidth = mState.outputFrameSize.width / 2u; else mState.outputPackTextureWidth = mState.outputFrameSize.width; if (InputIsTenBit()) { int deckLinkInputRowBytes = 0; if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK) mState.inputFrameRowBytes = static_cast(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: input none, output " << VideoIOPixelFormatName(mState.outputPixelFormat) << "."; if (!mState.formatStatusMessage.empty()) status << " " << mState.formatStatusMessage; mState.formatStatusMessage = status.str(); return true; } bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) { mOutputFrameCallback = std::move(callback); BMDDisplayMode outputDisplayMode = bmdModeUnknown; if (!DeckLinkDisplayModeForVideoFormat(outputVideoMode, outputDisplayMode)) { error = "DeckLink output setup failed while mapping " + outputVideoMode.displayName + " to a DeckLink display mode."; return false; } if (output->EnableVideoOutput(outputDisplayMode, 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. Enable Output alpha in host config to request it."; } const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy); mPlayoutPolicy = policy; for (unsigned i = 0; i < policy.outputFramePoolSize; i++) { CComPtr 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& 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 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) { ++mTelemetry.scheduleFailureCount; 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(); mTelemetry.scheduleCallMilliseconds = std::chrono::duration_cast>(scheduleEnd - scheduleStart).count(); if (result != S_OK) ++mTelemetry.scheduleFailureCount; RefreshBufferedVideoFrameCount(); return result == S_OK; } void DeckLinkSession::UpdateScheduleLeadTelemetry() { if (output == nullptr) { mTelemetry.scheduleLeadAvailable = false; return; } BMDTimeValue streamTime = 0; double playbackSpeed = 0.0; if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0) { mTelemetry.scheduleLeadAvailable = false; return; } const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0 ? static_cast(streamTime / mScheduler.FrameDuration()) : 0; const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex(); mTelemetry.scheduleLeadAvailable = true; mTelemetry.playbackStreamTime = streamTime; mTelemetry.playbackFrameIndex = playbackFrameIndex; mTelemetry.nextScheduleFrameIndex = nextScheduleFrameIndex; mTelemetry.scheduleLeadFrames = static_cast(nextScheduleFrameIndex) - static_cast(playbackFrameIndex); } void DeckLinkSession::MaybeRealignScheduleCursorForLowLead() { if (!mTelemetry.scheduleLeadAvailable) return; if (mTelemetry.scheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames) { mProactiveScheduleRealignmentArmed = true; return; } if (!mProactiveScheduleRealignmentArmed || mTelemetry.scheduleLeadFrames > 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); ++mTelemetry.scheduleRealignmentCount; UpdateScheduleLeadTelemetry(); } bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame) { if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0) return false; CComPtr videoBuffer; videoBuffer.Attach(new (std::nothrow) SystemMemoryDeckLinkVideoBuffer( frame.bytes, static_cast(frame.rowBytes) * static_cast(frame.height))); if (videoBuffer == nullptr) return false; CComPtr 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 lock(mScheduledSystemFrameMutex); mScheduledSystemFrameBuffers[scheduledFrame] = frame.bytes; } if (ScheduleFrame(outputVideoFrame)) return true; { std::lock_guard lock(mScheduledSystemFrameMutex); mScheduledSystemFrameBuffers.erase(scheduledFrame); } return false; } bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame) { if (outputVideoFrame == nullptr) return false; CComPtr 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) { mTelemetry.actualBufferedFramesAvailable = false; return; } unsigned int bufferedFrameCount = 0; if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK) { mTelemetry.actualBufferedFrames = bufferedFrameCount; mTelemetry.actualBufferedFramesAvailable = true; } else { mTelemetry.actualBufferedFramesAvailable = false; } } bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame) { CComPtr outputVideoFrame; return AcquireNextOutputVideoFrame(outputVideoFrame) && PopulateOutputFrame(outputVideoFrame, frame); } void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame) { IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast(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(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::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::Stop() { if (keyer != nullptr) { keyer->Disable(); mState.externalKeyingActive = false; } if (output) { output->StopScheduledPlayback(0, NULL, 0); output->DisableVideoOutput(); } return true; } void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult) { RefreshBufferedVideoFrameCount(); void* completedSystemBuffer = nullptr; if (completedFrame != nullptr) { bool externalSystemFrame = false; { std::lock_guard lock(mScheduledSystemFrameMutex); auto externalFrame = mScheduledSystemFrameBuffers.find(completedFrame); if (externalFrame != mScheduledSystemFrameBuffers.end()) { completedSystemBuffer = externalFrame->second; mScheduledSystemFrameBuffers.erase(externalFrame); externalSystemFrame = true; } } if (!externalSystemFrame) { CComPtr reusableFrame; if (completedFrame->QueryInterface(IID_IDeckLinkMutableVideoFrame, reinterpret_cast(&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; } }