#include "DeckLinkSession.h" #include "GlRenderConstants.h" #include #include #include #include #include #include #include namespace { 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 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 deckLinkIterator; CComPtr inputMode; CComPtr outputMode; mState.inputDisplayModeName = videoModes.input.displayName; mState.outputDisplayModeName = videoModes.output.displayName; 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; } 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 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 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(outputMode->GetWidth()), static_cast(outputMode->GetHeight()) }; mState.inputFrameSize = inputMode ? FrameSize{ static_cast(inputMode->GetWidth()), static_cast(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); 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(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(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."; } for (int i = 0; i < 10; 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::BeginOutputFrame(VideoIOOutputFrame& frame) { CComPtr outputVideoFrame = outputVideoFrameQueue.front(); outputVideoFrameQueue.push_back(outputVideoFrame); outputVideoFrameQueue.pop_front(); 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; frame.nativeFrame = outputVideoFrame.p; frame.nativeBuffer = outputVideoFrameBuffer.Detach(); return true; } 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; } void DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult) { mScheduler.AccountForCompletionResult(completionResult); } bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame) { IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast(frame.nativeFrame); const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime(); if (outputVideoFrame == nullptr || output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK) return false; return true; } bool DeckLinkSession::Start() { mScheduler.Reset(); 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; } for (unsigned i = 0; i < kPrerollFrameCount; i++) { CComPtr outputVideoFrame = outputVideoFrameQueue.front(); outputVideoFrameQueue.push_back(outputVideoFrame); outputVideoFrameQueue.pop_front(); CComPtr outputVideoFrameBuffer; if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK) { MessageBoxA(NULL, "Could not query the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR); return false; } if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK) { MessageBoxA(NULL, "Could not write to the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR); return false; } void* pFrame = nullptr; outputVideoFrameBuffer->GetBytes((void**)&pFrame); memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height); outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime(); if (output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK) { MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR); return false; } } if (input) { if (input->StartStreams() != S_OK) { MessageBoxA(NULL, "Could not start the DeckLink input stream.", "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; } return true; } 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 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(inputFrame->GetWidth()); frame.height = static_cast(inputFrame->GetHeight()); frame.pixelFormat = mState.inputPixelFormat; frame.hasNoInputSource = hasNoInputSource; mInputFrameCallback(frame); inputFrameBuffer->EndAccess(bmdBufferAccessRead); } void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame*, BMDOutputFrameCompletionResult completionResult) { if (!mOutputFrameCallback) return; VideoIOCompletion completion; switch (completionResult) { case bmdOutputFrameDisplayedLate: completion.result = VideoIOCompletionResult::DisplayedLate; break; case bmdOutputFrameDropped: completion.result = VideoIOCompletionResult::Dropped; break; case bmdOutputFrameFlushed: completion.result = VideoIOCompletionResult::Flushed; break; case bmdOutputFrameCompleted: completion.result = VideoIOCompletionResult::Completed; break; default: completion.result = VideoIOCompletionResult::Unknown; break; } mOutputFrameCallback(completion); }