From ebbc11bb3440ea142c30c04985bb649bbcff8931 Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 8 May 2026 16:27:40 +1000 Subject: [PATCH] Decklink abstraction --- CMakeLists.txt | 52 +++- .../LoopThroughWithOpenGLCompositing.vcxproj | 19 +- ...roughWithOpenGLCompositing.vcxproj.filters | 25 +- .../decklink/DeckLinkFrameTransfer.cpp | 10 +- .../decklink/DeckLinkFrameTransfer.h | 10 +- .../decklink/DeckLinkSession.cpp | 284 ++++++++++++------ .../decklink/DeckLinkSession.h | 109 +++---- .../decklink/DeckLinkVideoIOFormat.cpp | 24 ++ .../decklink/DeckLinkVideoIOFormat.h | 7 + .../gl/OpenGLComposite.cpp | 117 ++++---- .../gl/OpenGLComposite.h | 15 +- ...LinkBridge.cpp => OpenGLVideoIOBridge.cpp} | 112 +++---- ...DeckLinkBridge.h => OpenGLVideoIOBridge.h} | 23 +- .../runtime/RuntimeHost.cpp | 19 ++ .../runtime/RuntimeHost.h | 3 + .../{decklink => videoio}/VideoIOFormat.cpp | 42 ++- .../{decklink => videoio}/VideoIOFormat.h | 9 +- .../videoio/VideoIOTypes.h | 137 +++++++++ .../videoio/VideoPlayoutScheduler.cpp | 37 +++ .../videoio/VideoPlayoutScheduler.h | 22 ++ tests/VideoIODeviceFakeTests.cpp | 151 ++++++++++ tests/VideoIOFormatTests.cpp | 5 + tests/VideoPlayoutSchedulerTests.cpp | 81 +++++ 23 files changed, 971 insertions(+), 342 deletions(-) create mode 100644 apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.h rename apps/LoopThroughWithOpenGLCompositing/gl/{OpenGLDeckLinkBridge.cpp => OpenGLVideoIOBridge.cpp} (55%) rename apps/LoopThroughWithOpenGLCompositing/gl/{OpenGLDeckLinkBridge.h => OpenGLVideoIOBridge.h} (70%) rename apps/LoopThroughWithOpenGLCompositing/{decklink => videoio}/VideoIOFormat.cpp (85%) rename apps/LoopThroughWithOpenGLCompositing/{decklink => videoio}/VideoIOFormat.h (83%) create mode 100644 apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h create mode 100644 apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h create mode 100644 tests/VideoIODeviceFakeTests.cpp create mode 100644 tests/VideoPlayoutSchedulerTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 057059d..0f0ec17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,8 +46,8 @@ set(APP_SOURCES "${APP_DIR}/decklink/DeckLinkFrameTransfer.h" "${APP_DIR}/decklink/DeckLinkSession.cpp" "${APP_DIR}/decklink/DeckLinkSession.h" - "${APP_DIR}/decklink/VideoIOFormat.cpp" - "${APP_DIR}/decklink/VideoIOFormat.h" + "${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp" + "${APP_DIR}/decklink/DeckLinkVideoIOFormat.h" "${APP_DIR}/gl/GLExtensions.cpp" "${APP_DIR}/gl/GLExtensions.h" "${APP_DIR}/gl/GlobalParamsBuffer.cpp" @@ -59,12 +59,12 @@ set(APP_SOURCES "${APP_DIR}/gl/OpenGLComposite.cpp" "${APP_DIR}/gl/OpenGLComposite.h" "${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp" - "${APP_DIR}/gl/OpenGLDeckLinkBridge.cpp" - "${APP_DIR}/gl/OpenGLDeckLinkBridge.h" "${APP_DIR}/gl/OpenGLRenderPass.cpp" "${APP_DIR}/gl/OpenGLRenderPass.h" "${APP_DIR}/gl/OpenGLRenderer.cpp" "${APP_DIR}/gl/OpenGLRenderer.h" + "${APP_DIR}/gl/OpenGLVideoIOBridge.cpp" + "${APP_DIR}/gl/OpenGLVideoIOBridge.h" "${APP_DIR}/gl/OpenGLShaderPrograms.cpp" "${APP_DIR}/gl/OpenGLShaderPrograms.h" "${APP_DIR}/gl/PngScreenshotWriter.cpp" @@ -104,6 +104,11 @@ set(APP_SOURCES "${APP_DIR}/stdafx.cpp" "${APP_DIR}/stdafx.h" "${APP_DIR}/targetver.h" + "${APP_DIR}/videoio/VideoIOFormat.cpp" + "${APP_DIR}/videoio/VideoIOFormat.h" + "${APP_DIR}/videoio/VideoIOTypes.h" + "${APP_DIR}/videoio/VideoPlayoutScheduler.cpp" + "${APP_DIR}/videoio/VideoPlayoutScheduler.h" ) add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES}) @@ -116,6 +121,7 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE "${APP_DIR}/platform" "${APP_DIR}/runtime" "${APP_DIR}/shader" + "${APP_DIR}/videoio" ) target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE @@ -244,13 +250,15 @@ endif() add_test(NAME OscServerTests COMMAND OscServerTests) add_executable(VideoIOFormatTests - "${APP_DIR}/decklink/VideoIOFormat.cpp" + "${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp" + "${APP_DIR}/videoio/VideoIOFormat.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp" ) target_include_directories(VideoIOFormatTests PRIVATE "${APP_DIR}" "${APP_DIR}/decklink" + "${APP_DIR}/videoio" ) if(MSVC) @@ -259,6 +267,40 @@ endif() add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests) +add_executable(VideoPlayoutSchedulerTests + "${APP_DIR}/videoio/VideoPlayoutScheduler.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoPlayoutSchedulerTests.cpp" +) + +target_include_directories(VideoPlayoutSchedulerTests PRIVATE + "${APP_DIR}" + "${APP_DIR}/decklink" + "${APP_DIR}/videoio" +) + +if(MSVC) + target_compile_options(VideoPlayoutSchedulerTests PRIVATE /W3) +endif() + +add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests) + +add_executable(VideoIODeviceFakeTests + "${APP_DIR}/videoio/VideoIOFormat.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp" +) + +target_include_directories(VideoIODeviceFakeTests PRIVATE + "${APP_DIR}" + "${APP_DIR}/decklink" + "${APP_DIR}/videoio" +) + +if(MSVC) + target_compile_options(VideoIODeviceFakeTests PRIVATE /W3) +endif() + +add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests) + install(TARGETS LoopThroughWithOpenGLCompositing RUNTIME DESTINATION "." ) diff --git a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj index 6b6fabe..2b557f6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj +++ b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj @@ -89,7 +89,7 @@ Disabled - .;control;decklink;gl;%(AdditionalIncludeDirectories) + .;control;decklink;gl;videoio;%(AdditionalIncludeDirectories) WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL @@ -111,7 +111,7 @@ Disabled - .;control;decklink;gl;%(AdditionalIncludeDirectories) + .;control;decklink;gl;videoio;%(AdditionalIncludeDirectories) WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) EnableFastChecks MultiThreadedDebugDLL @@ -131,7 +131,7 @@ MaxSpeed true - .;control;decklink;gl;%(AdditionalIncludeDirectories) + .;control;decklink;gl;videoio;%(AdditionalIncludeDirectories) WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) MultiThreadedDLL true @@ -156,7 +156,7 @@ MaxSpeed true - .;control;decklink;gl;%(AdditionalIncludeDirectories) + .;control;decklink;gl;videoio;%(AdditionalIncludeDirectories) WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) MultiThreadedDLL true @@ -184,6 +184,7 @@ + Create Create @@ -193,8 +194,10 @@ - + + + @@ -206,13 +209,17 @@ + - + + + + diff --git a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters index 0d6bc94..e42ac2c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters +++ b/apps/LoopThroughWithOpenGLCompositing/LoopThroughWithOpenGLCompositing.vcxproj.filters @@ -45,6 +45,9 @@ Source Files + + Source Files + Source Files @@ -57,12 +60,18 @@ Source Files - + Source Files Source Files + + Source Files + + + Source Files + @@ -92,6 +101,9 @@ Header Files + + Header Files + Header Files @@ -107,12 +119,21 @@ Header Files - + Header Files Header Files + + Header Files + + + Header Files + + + Header Files + diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.cpp b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.cpp index 9f81db8..8bfe19f 100644 --- a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.cpp @@ -1,11 +1,11 @@ #include "DeckLinkFrameTransfer.h" -#include "OpenGLComposite.h" +#include "DeckLinkSession.h" //////////////////////////////////////////// // DeckLink Capture Delegate Class //////////////////////////////////////////// -CaptureDelegate::CaptureDelegate(OpenGLComposite* pOwner) : +CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) : m_pOwner(pOwner), mRefCount(1) { @@ -39,7 +39,7 @@ HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputF } bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource; - m_pOwner->VideoFrameArrived(inputFrame, hasNoInputSource); + m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource); return S_OK; } @@ -51,7 +51,7 @@ HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvent //////////////////////////////////////////// // DeckLink Playout Delegate Class //////////////////////////////////////////// -PlayoutDelegate::PlayoutDelegate(OpenGLComposite* pOwner) : +PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) : m_pOwner(pOwner), mRefCount(1) { @@ -94,7 +94,7 @@ HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedF OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n"); } - m_pOwner->PlayoutFrameCompleted(completedFrame, result); + m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result); return S_OK; } diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.h b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.h index 9e12a0f..de79944 100644 --- a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.h +++ b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkFrameTransfer.h @@ -6,18 +6,18 @@ #include "DeckLinkAPI_h.h" -class OpenGLComposite; +class DeckLinkSession; //////////////////////////////////////////// // Capture Delegate Class //////////////////////////////////////////// class CaptureDelegate : public IDeckLinkInputCallback { - OpenGLComposite* m_pOwner; + DeckLinkSession* m_pOwner; LONG mRefCount; public: - CaptureDelegate(OpenGLComposite* pOwner); + CaptureDelegate(DeckLinkSession* pOwner); // IUnknown needs only a dummy implementation virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv); @@ -33,11 +33,11 @@ public: //////////////////////////////////////////// class PlayoutDelegate : public IDeckLinkVideoOutputCallback { - OpenGLComposite* m_pOwner; + DeckLinkSession* m_pOwner; LONG mRefCount; public: - PlayoutDelegate(OpenGLComposite* pOwner); + PlayoutDelegate(DeckLinkSession* pOwner); // IUnknown needs only a dummy implementation virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv); diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.cpp b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.cpp index a978f86..6bfeed2 100644 --- a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include namespace @@ -82,7 +83,7 @@ void DeckLinkSession::ReleaseResources() if (keyer != nullptr) { keyer->Disable(); - externalKeyingActive = false; + mState.externalKeyingActive = false; } keyer.Release(); @@ -97,8 +98,8 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM CComPtr inputMode; CComPtr outputMode; - inputDisplayModeName = videoModes.input.displayName; - outputDisplayModeName = videoModes.output.displayName; + 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)) @@ -150,9 +151,9 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM output.Release(); else { - outputModelName = modelName; - supportsInternalKeying = deviceSupportsInternalKeying; - supportsExternalKeying = deviceSupportsExternalKeying; + mState.outputModelName = modelName; + mState.supportsInternalKeying = deviceSupportsInternalKeying; + mState.supportsExternalKeying = deviceSupportsExternalKeying; } } @@ -200,18 +201,24 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM return false; } - outputFrameSize = { static_cast(outputMode->GetWidth()), static_cast(outputMode->GetHeight()) }; - inputFrameSize = inputMode + mState.outputFrameSize = { static_cast(outputMode->GetWidth()), static_cast(outputMode->GetHeight()) }; + mState.inputFrameSize = inputMode ? FrameSize{ static_cast(inputMode->GetWidth()), static_cast(inputMode->GetHeight()) } - : outputFrameSize; + : mState.outputFrameSize; if (!input) - inputDisplayModeName = "No input - black frame"; + mState.inputDisplayModeName = "No input - black frame"; + BMDTimeValue frameDuration = 0; + BMDTimeScale frameTimescale = 0; outputMode->GetFrameRate(&frameDuration, &frameTimescale); + mScheduler.Configure(frameDuration, frameTimescale); + mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds(); - inputFrameRowBytes = inputFrameSize.width * 2u; - outputFrameRowBytes = outputFrameSize.width * 4u; - captureTextureWidth = inputFrameSize.width / 2u; - outputPackTextureWidth = outputFrameSize.width; + 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; } @@ -224,95 +231,95 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo return false; } - formatStatusMessage.clear(); + mState.formatStatusMessage.clear(); const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV); - inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8; + mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8; if (input != nullptr && !inputTenBitSupported) - formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. "; + 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); - outputPixelFormat = ChoosePreferredVideoIOFormat(outputTenBitSupported); + mState.outputPixelFormat = outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8; if (!outputTenBitSupported) - formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. "; + 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(OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA, outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK) + if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK) { error = "DeckLink output setup failed while calculating output row bytes."; return false; } - outputFrameRowBytes = static_cast(deckLinkOutputRowBytes); - outputPackTextureWidth = OutputIsTenBit() - ? PackedTextureWidthFromRowBytes(outputFrameRowBytes) - : outputFrameSize.width; + mState.outputFrameRowBytes = static_cast(deckLinkOutputRowBytes); + mState.outputPackTextureWidth = OutputIsTenBit() + ? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes) + : mState.outputFrameSize.width; if (InputIsTenBit()) { int deckLinkInputRowBytes = 0; - if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, inputFrameSize.width, &deckLinkInputRowBytes) == S_OK) - inputFrameRowBytes = static_cast(deckLinkInputRowBytes); + if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK) + mState.inputFrameRowBytes = static_cast(deckLinkInputRowBytes); else - inputFrameRowBytes = MinimumV210RowBytes(inputFrameSize.width); + mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width); } else { - inputFrameRowBytes = inputFrameSize.width * 2u; + mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u; } - captureTextureWidth = InputIsTenBit() - ? PackedTextureWidthFromRowBytes(inputFrameRowBytes) - : inputFrameSize.width / 2u; + mState.captureTextureWidth = InputIsTenBit() + ? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes) + : mState.inputFrameSize.width / 2u; std::ostringstream status; - status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(inputPixelFormat) : "none") - << ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA") << "."; - if (!formatStatusMessage.empty()) - status << " " << formatStatusMessage; - formatStatusMessage = status.str(); + 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(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error) +bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) { - (void)hdc; - (void)hglrc; + mInputFrameCallback = std::move(callback); if (!input) { - hasNoInputSource = true; - inputDisplayModeName = "No input - black frame"; + mState.hasInputSource = false; + mState.inputDisplayModeName = "No input - black frame"; return true; } - const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(inputPixelFormat); + const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat); if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK) { - if (inputPixelFormat == VideoIOPixelFormat::V210) + if (mState.inputPixelFormat == VideoIOPixelFormat::V210) { OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n"); - inputPixelFormat = VideoIOPixelFormat::Uyvy8; - inputFrameRowBytes = inputFrameSize.width * 2u; - captureTextureWidth = inputFrameSize.width / 2u; + 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(inputPixelFormat) - << ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA") + status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat) + << ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ". DeckLink 10-bit input enable failed; using 8-bit capture."; - formatStatusMessage = status.str(); + mState.formatStatusMessage = status.str(); goto input_enabled; } } OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n"); input.Release(); - hasNoInputSource = true; - inputDisplayModeName = "No input - black frame"; + mState.hasInputDevice = false; + mState.hasInputSource = false; + mState.inputDisplayModeName = "No input - black frame"; return true; } input_enabled: - captureDelegate.Attach(new (std::nothrow) CaptureDelegate(owner)); + captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this)); if (captureDelegate == nullptr) { error = "DeckLink input setup failed while creating the capture callback."; @@ -327,10 +334,9 @@ input_enabled: return true; } -bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) +bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) { - (void)hdc; - (void)hglrc; + mOutputFrameCallback = std::move(callback); if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK) { @@ -339,39 +345,39 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl } if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL) - keyerInterfaceAvailable = true; + mState.keyerInterfaceAvailable = true; if (externalKeyingEnabled) { - if (!supportsExternalKeying) + if (!mState.supportsExternalKeying) { - statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support."; + mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support."; } - else if (!keyerInterfaceAvailable) + else if (!mState.keyerInterfaceAvailable) { - statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface."; + 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) { - statusMessage = "External keying was requested, but enabling the DeckLink keyer failed."; + mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed."; } else { - externalKeyingActive = true; - statusMessage = "External keying is active on the selected DeckLink output."; + mState.externalKeyingActive = true; + mState.statusMessage = "External keying is active on the selected DeckLink output."; } } - else if (supportsExternalKeying) + else if (mState.supportsExternalKeying) { - statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it."; + 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 = OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA; - if (output->CreateVideoFrame(outputFrameSize.width, outputFrameSize.height, outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK) + 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; @@ -380,7 +386,7 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl outputVideoFrameQueue.push_back(outputFrame); } - playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(owner)); + playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this)); if (playoutDelegate == nullptr) { error = "DeckLink output setup failed while creating the playout callback."; @@ -393,45 +399,73 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl return false; } - if (!formatStatusMessage.empty()) - statusMessage = statusMessage.empty() ? formatStatusMessage : formatStatusMessage + " " + statusMessage; + if (!mState.formatStatusMessage.empty()) + mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage; return true; } double DeckLinkSession::FrameBudgetMilliseconds() const { - return frameTimescale != 0 - ? (static_cast(frameDuration) * 1000.0) / static_cast(frameTimescale) - : 0.0; + return mScheduler.FrameBudgetMilliseconds(); } -IDeckLinkMutableVideoFrame* DeckLinkSession::RotateOutputFrame() +bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame) { CComPtr outputVideoFrame = outputVideoFrameQueue.front(); outputVideoFrameQueue.push_back(outputVideoFrame); outputVideoFrameQueue.pop_front(); - return outputVideoFrame.p; -} -void DeckLinkSession::AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult) -{ - if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped) - totalPlayoutFrames += 2; -} - -bool DeckLinkSession::ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame) -{ - if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK) + 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; - totalPlayoutFrames++; return true; } bool DeckLinkSession::Start() { - totalPlayoutFrames = 0; + mScheduler.Reset(); if (!output) { MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR); @@ -462,19 +496,18 @@ bool DeckLinkSession::Start() return false; } - void* pFrame; + void* pFrame = nullptr; outputVideoFrameBuffer->GetBytes((void**)&pFrame); - memset(pFrame, 0, outputVideoFrame->GetRowBytes() * outputFrameSize.height); + memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height); outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); - if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK) + 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; } - - totalPlayoutFrames++; } if (input) @@ -485,7 +518,7 @@ bool DeckLinkSession::Start() return false; } } - if (output->StartScheduledPlayback(0, frameTimescale, 1.0) != S_OK) + 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; @@ -499,7 +532,7 @@ bool DeckLinkSession::Stop() if (keyer != nullptr) { keyer->Disable(); - externalKeyingActive = false; + mState.externalKeyingActive = false; } if (input) @@ -516,3 +549,66 @@ bool DeckLinkSession::Stop() 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); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.h b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.h index 11a1464..4344e38 100644 --- a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.h +++ b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkSession.h @@ -3,7 +3,10 @@ #include "DeckLinkAPI_h.h" #include "DeckLinkDisplayMode.h" #include "DeckLinkFrameTransfer.h" +#include "DeckLinkVideoIOFormat.h" #include "VideoIOFormat.h" +#include "VideoIOTypes.h" +#include "VideoPlayoutScheduler.h" #include #include @@ -11,51 +14,56 @@ class OpenGLComposite; -class DeckLinkSession +class DeckLinkSession : public VideoIODevice { public: DeckLinkSession() = default; ~DeckLinkSession(); - void ReleaseResources(); - bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error); - bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error); - bool ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error); - bool ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error); - bool Start(); - bool Stop(); + void ReleaseResources() override; + bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override; + bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) override; + bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override; + bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override; + bool Start() override; + bool Stop() override; - bool HasInputDevice() const { return input != nullptr; } - bool HasInputSource() const { return !hasNoInputSource; } - void SetInputSourceMissing(bool missing) { hasNoInputSource = missing; } - bool InputOutputDimensionsDiffer() const { return inputFrameSize != outputFrameSize; } - const FrameSize& InputFrameSize() const { return inputFrameSize; } - const FrameSize& OutputFrameSize() const { return outputFrameSize; } - unsigned InputFrameWidth() const { return inputFrameSize.width; } - unsigned InputFrameHeight() const { return inputFrameSize.height; } - unsigned OutputFrameWidth() const { return outputFrameSize.width; } - unsigned OutputFrameHeight() const { return outputFrameSize.height; } - VideoIOPixelFormat InputPixelFormat() const { return inputPixelFormat; } - VideoIOPixelFormat OutputPixelFormat() const { return outputPixelFormat; } - bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(inputPixelFormat); } - bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(outputPixelFormat); } - unsigned InputFrameRowBytes() const { return inputFrameRowBytes; } - unsigned OutputFrameRowBytes() const { return outputFrameRowBytes; } - unsigned CaptureTextureWidth() const { return captureTextureWidth; } - unsigned OutputPackTextureWidth() const { return outputPackTextureWidth; } - const std::string& FormatStatusMessage() const { return formatStatusMessage; } - const std::string& InputDisplayModeName() const { return inputDisplayModeName; } - const std::string& OutputModelName() const { return outputModelName; } - bool SupportsInternalKeying() const { return supportsInternalKeying; } - bool SupportsExternalKeying() const { return supportsExternalKeying; } - bool KeyerInterfaceAvailable() const { return keyerInterfaceAvailable; } - bool ExternalKeyingActive() const { return externalKeyingActive; } - const std::string& StatusMessage() const { return statusMessage; } - void SetStatusMessage(const std::string& message) { statusMessage = message; } + bool HasInputDevice() const { return mState.hasInputDevice; } + bool HasInputSource() const { return mState.hasInputSource; } + void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; } + bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; } + const FrameSize& InputFrameSize() const { return mState.inputFrameSize; } + const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; } + unsigned InputFrameWidth() const { return mState.inputFrameSize.width; } + unsigned InputFrameHeight() const { return mState.inputFrameSize.height; } + unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; } + unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; } + VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; } + VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; } + bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); } + bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); } + unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; } + unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; } + unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; } + unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; } + const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; } + const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; } + const std::string& OutputModelName() const { return mState.outputModelName; } + bool SupportsInternalKeying() const { return mState.supportsInternalKeying; } + bool SupportsExternalKeying() const { return mState.supportsExternalKeying; } + bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; } + bool ExternalKeyingActive() const { return mState.externalKeyingActive; } + const std::string& StatusMessage() const { return mState.statusMessage; } + void SetStatusMessage(const std::string& message) { mState.statusMessage = message; } + const VideoIOState& State() const override { return mState; } + VideoIOState& MutableState() override { return mState; } double FrameBudgetMilliseconds() const; - IDeckLinkMutableVideoFrame* RotateOutputFrame(); - void AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult); - bool ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame); + void AccountForCompletionResult(VideoIOCompletionResult completionResult) override; + bool BeginOutputFrame(VideoIOOutputFrame& frame) override; + void EndOutputFrame(VideoIOOutputFrame& frame) override; + bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override; + void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource); + void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult); private: CComPtr captureDelegate; @@ -64,25 +72,8 @@ private: CComPtr output; CComPtr keyer; std::deque> outputVideoFrameQueue; - BMDTimeValue frameDuration = 0; - BMDTimeScale frameTimescale = 0; - unsigned totalPlayoutFrames = 0; - FrameSize inputFrameSize; - FrameSize outputFrameSize; - VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8; - VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Uyvy8; - unsigned inputFrameRowBytes = 0; - unsigned outputFrameRowBytes = 0; - unsigned captureTextureWidth = 0; - unsigned outputPackTextureWidth = 0; - std::string inputDisplayModeName = "1080p59.94"; - std::string outputDisplayModeName = "1080p59.94"; - bool hasNoInputSource = true; - std::string outputModelName; - bool supportsInternalKeying = false; - bool supportsExternalKeying = false; - bool keyerInterfaceAvailable = false; - bool externalKeyingActive = false; - std::string statusMessage; - std::string formatStatusMessage; + VideoIOState mState; + VideoPlayoutScheduler mScheduler; + InputFrameCallback mInputFrameCallback; + OutputFrameCallback mOutputFrameCallback; }; diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.cpp b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.cpp new file mode 100644 index 0000000..13b7d71 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.cpp @@ -0,0 +1,24 @@ +#include "DeckLinkVideoIOFormat.h" + +BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format) +{ + switch (format) + { + case VideoIOPixelFormat::V210: + return bmdFormat10BitYUV; + case VideoIOPixelFormat::Bgra8: + return bmdFormat8BitBGRA; + case VideoIOPixelFormat::Uyvy8: + default: + return bmdFormat8BitYUV; + } +} + +VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format) +{ + if (format == bmdFormat10BitYUV) + return VideoIOPixelFormat::V210; + if (format == bmdFormat8BitBGRA) + return VideoIOPixelFormat::Bgra8; + return VideoIOPixelFormat::Uyvy8; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.h b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.h new file mode 100644 index 0000000..f6106a3 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/decklink/DeckLinkVideoIOFormat.h @@ -0,0 +1,7 @@ +#pragma once + +#include "DeckLinkAPI_h.h" +#include "VideoIOFormat.h" + +BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format); +VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp index 515abc6..1432e4e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp @@ -3,9 +3,9 @@ #include "OpenGLComposite.h" #include "GLExtensions.h" #include "GlRenderConstants.h" -#include "OpenGLDeckLinkBridge.h" #include "OpenGLRenderPass.h" #include "OpenGLShaderPrograms.h" +#include "OpenGLVideoIOBridge.h" #include "PngScreenshotWriter.h" #include "RuntimeServices.h" #include "ShaderBuildQueue.h" @@ -22,15 +22,15 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC), - mDeckLink(std::make_unique()), + mVideoIO(std::make_unique()), mRenderer(std::make_unique()), mUseCommittedLayerStates(false), mScreenshotRequested(false) { InitializeCriticalSection(&pMutex); mRuntimeHost = std::make_unique(); - mDeckLinkBridge = std::make_unique( - *mDeckLink, + mVideoIOBridge = std::make_unique( + *mVideoIO, *mRenderer, *mRuntimeHost, pMutex, @@ -51,13 +51,18 @@ OpenGLComposite::~OpenGLComposite() mRuntimeServices->Stop(); if (mShaderBuildQueue) mShaderBuildQueue->Stop(); - mDeckLink->ReleaseResources(); + mVideoIO->ReleaseResources(); mRenderer->DestroyResources(); DeleteCriticalSection(&pMutex); } bool OpenGLComposite::InitDeckLink() +{ + return InitVideoIO(); +} + +bool OpenGLComposite::InitVideoIO() { VideoFormatSelection videoModes; std::string initFailureReason; @@ -87,7 +92,7 @@ bool OpenGLComposite::InitDeckLink() } } - if (!mDeckLink->DiscoverDevicesAndModes(videoModes, initFailureReason)) + if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason)) { const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application." ? "This application requires the DeckLink drivers installed." @@ -95,7 +100,7 @@ bool OpenGLComposite::InitDeckLink() MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR); return false; } - if (!mDeckLink->SelectPreferredFormats(videoModes, initFailureReason)) + if (!mVideoIO->SelectPreferredFormats(videoModes, initFailureReason)) goto error; if (! CheckOpenGLExtensions()) @@ -110,38 +115,38 @@ bool OpenGLComposite::InitDeckLink() goto error; } - PublishDeckLinkOutputStatus(mDeckLink->OutputModelName().empty() + PublishVideoIOStatus(mVideoIO->OutputModelName().empty() ? "DeckLink output device selected." - : ("Selected output device: " + mDeckLink->OutputModelName())); + : ("Selected output device: " + mVideoIO->OutputModelName())); // Resize window to match output video frame, but scale large formats down by half for viewing. - if (mDeckLink->OutputFrameWidth() < 1920) - resizeWindow(mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight()); + if (mVideoIO->OutputFrameWidth() < 1920) + resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight()); else - resizeWindow(mDeckLink->OutputFrameWidth() / 2, mDeckLink->OutputFrameHeight() / 2); + resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2); - if (!mDeckLink->ConfigureInput(this, hGLDC, hGLRC, videoModes.input, initFailureReason)) + if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason)) { goto error; } - if (!mDeckLink->HasInputDevice() && mRuntimeHost) + if (!mVideoIO->HasInputDevice() && mRuntimeHost) { - mRuntimeHost->SetSignalStatus(false, mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), mDeckLink->InputDisplayModeName()); + mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName()); } - if (!mDeckLink->ConfigureOutput(this, hGLDC, hGLRC, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason)) + if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason)) { goto error; } - PublishDeckLinkOutputStatus(mDeckLink->StatusMessage()); + PublishVideoIOStatus(mVideoIO->StatusMessage()); return true; error: if (!initFailureReason.empty()) MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR); - mDeckLink->ReleaseResources(); + mVideoIO->ReleaseResources(); return false; } @@ -153,7 +158,7 @@ void OpenGLComposite::paintGL() return; } - mRenderer->PresentToWindow(hGLDC, mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight()); + mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight()); ValidateRect(hGLWnd, NULL); LeaveCriticalSection(&pMutex); } @@ -174,22 +179,23 @@ void OpenGLComposite::resizeWindow(int width, int height) } } -void OpenGLComposite::PublishDeckLinkOutputStatus(const std::string& statusMessage) +void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage) { if (!mRuntimeHost) return; if (!statusMessage.empty()) - mDeckLink->SetStatusMessage(statusMessage); + mVideoIO->SetStatusMessage(statusMessage); - mRuntimeHost->SetDeckLinkOutputStatus( - mDeckLink->OutputModelName(), - mDeckLink->SupportsInternalKeying(), - mDeckLink->SupportsExternalKeying(), - mDeckLink->KeyerInterfaceAvailable(), + mRuntimeHost->SetVideoIOStatus( + "decklink", + mVideoIO->OutputModelName(), + mVideoIO->SupportsInternalKeying(), + mVideoIO->SupportsExternalKeying(), + mVideoIO->KeyerInterfaceAvailable(), mRuntimeHost->ExternalKeyingEnabled(), - mDeckLink->ExternalKeyingActive(), - mDeckLink->StatusMessage()); + mVideoIO->ExternalKeyingActive(), + mVideoIO->StatusMessage()); } bool OpenGLComposite::InitOpenGLState() @@ -223,7 +229,7 @@ bool OpenGLComposite::InitOpenGLState() return false; } - if (!mShaderPrograms->CompileLayerPrograms(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage)) + if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage)) { MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK); return false; @@ -234,12 +240,12 @@ bool OpenGLComposite::InitOpenGLState() std::string rendererError; if (!mRenderer->InitializeResources( - mDeckLink->InputFrameWidth(), - mDeckLink->InputFrameHeight(), - mDeckLink->CaptureTextureWidth(), - mDeckLink->OutputFrameWidth(), - mDeckLink->OutputFrameHeight(), - mDeckLink->OutputPackTextureWidth(), + mVideoIO->InputFrameWidth(), + mVideoIO->InputFrameHeight(), + mVideoIO->CaptureTextureWidth(), + mVideoIO->OutputFrameWidth(), + mVideoIO->OutputFrameHeight(), + mVideoIO->OutputPackTextureWidth(), rendererError)) { MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK); @@ -251,20 +257,9 @@ bool OpenGLComposite::InitOpenGLState() return true; } -// DeckLink delegates still target OpenGLComposite; the bridge owns the per-frame work. -void OpenGLComposite::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource) -{ - mDeckLinkBridge->VideoFrameArrived(inputFrame, hasNoInputSource); -} - -void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult) -{ - mDeckLinkBridge->PlayoutFrameCompleted(completedFrame, completionResult); -} - bool OpenGLComposite::Start() { - return mDeckLink->Start(); + return mVideoIO->Start(); } bool OpenGLComposite::Stop() @@ -272,10 +267,10 @@ bool OpenGLComposite::Stop() if (mRuntimeServices) mRuntimeServices->Stop(); - const bool wasExternalKeyingActive = mDeckLink->ExternalKeyingActive(); - mDeckLink->Stop(); + const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive(); + mVideoIO->Stop(); if (wasExternalKeyingActive) - PublishDeckLinkOutputStatus("External keying has been disabled."); + PublishVideoIOStatus("External keying has been disabled."); return true; } @@ -303,7 +298,7 @@ void OpenGLComposite::renderEffect() { ProcessRuntimePollResults(); - const bool hasInputSource = mDeckLink->HasInputSource(); + const bool hasInputSource = mVideoIO->HasInputSource(); std::vector layerStates; if (mUseCommittedLayerStates) { @@ -313,7 +308,7 @@ void OpenGLComposite::renderEffect() } else if (mRuntimeHost) { - if (mRuntimeHost->TryGetLayerRenderStates(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), layerStates)) + if (mRuntimeHost->TryGetLayerRenderStates(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), layerStates)) { mCachedLayerRenderStates = layerStates; } @@ -327,10 +322,10 @@ void OpenGLComposite::renderEffect() mRenderPass->Render( hasInputSource, layerStates, - mDeckLink->InputFrameWidth(), - mDeckLink->InputFrameHeight(), - mDeckLink->CaptureTextureWidth(), - mDeckLink->InputPixelFormat(), + mVideoIO->InputFrameWidth(), + mVideoIO->InputFrameHeight(), + mVideoIO->CaptureTextureWidth(), + mVideoIO->InputPixelFormat(), historyCap, [this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) { return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error); @@ -345,8 +340,8 @@ void OpenGLComposite::ProcessScreenshotRequest() if (!mScreenshotRequested.exchange(false)) return; - const unsigned width = mDeckLink ? mDeckLink->OutputFrameWidth() : 0; - const unsigned height = mDeckLink ? mDeckLink->OutputFrameHeight() : 0; + const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0; + const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0; if (width == 0 || height == 0) return; @@ -426,7 +421,7 @@ bool OpenGLComposite::ProcessRuntimePollResults() return true; char compilerErrorMessage[1024] = {}; - if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage)) + if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage)) { mRuntimeHost->SetCompileStatus(false, compilerErrorMessage); mUseCommittedLayerStates = true; @@ -449,13 +444,13 @@ bool OpenGLComposite::ProcessRuntimePollResults() void OpenGLComposite::RequestShaderBuild() { - if (!mShaderBuildQueue || !mDeckLink) + if (!mShaderBuildQueue || !mVideoIO) return; mUseCommittedLayerStates = true; if (mRuntimeHost) mRuntimeHost->ClearReloadRequest(); - mShaderBuildQueue->RequestBuild(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight()); + mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight()); } void OpenGLComposite::broadcastRuntimeState() diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h index 18c7d18..b6d10b7 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h @@ -10,7 +10,6 @@ #include #include #include -#include "DeckLinkAPI_h.h" #include "GLExtensions.h" #include "OpenGLRenderer.h" @@ -25,8 +24,8 @@ #include #include -class DeckLinkSession; -class OpenGLDeckLinkBridge; +class VideoIODevice; +class OpenGLVideoIOBridge; class OpenGLRenderPass; class OpenGLShaderPrograms; class RuntimeServices; @@ -40,6 +39,7 @@ public: ~OpenGLComposite(); bool InitDeckLink(); + bool InitVideoIO(); bool Start(); bool Stop(); bool ReloadShader(); @@ -65,13 +65,10 @@ public: void resizeGL(WORD width, WORD height); void paintGL(); - void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource); - void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result); - private: void resizeWindow(int width, int height); bool CheckOpenGLExtensions(); - void PublishDeckLinkOutputStatus(const std::string& statusMessage); + void PublishVideoIOStatus(const std::string& statusMessage); using LayerProgram = OpenGLRenderer::LayerProgram; HWND hGLWnd; @@ -79,10 +76,10 @@ private: HGLRC hGLRC; CRITICAL_SECTION pMutex; - std::unique_ptr mDeckLink; + std::unique_ptr mVideoIO; std::unique_ptr mRenderer; std::unique_ptr mRuntimeHost; - std::unique_ptr mDeckLinkBridge; + std::unique_ptr mVideoIOBridge; std::unique_ptr mRenderPass; std::unique_ptr mShaderPrograms; std::unique_ptr mShaderBuildQueue; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.cpp similarity index 55% rename from apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.cpp rename to apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.cpp index a455724..0911611 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.cpp @@ -1,14 +1,13 @@ -#include "OpenGLDeckLinkBridge.h" +#include "OpenGLVideoIOBridge.h" -#include "DeckLinkSession.h" #include "OpenGLRenderer.h" #include "RuntimeHost.h" #include #include -OpenGLDeckLinkBridge::OpenGLDeckLinkBridge( - DeckLinkSession& deckLink, +OpenGLVideoIOBridge::OpenGLVideoIOBridge( + VideoIODevice& videoIO, OpenGLRenderer& renderer, RuntimeHost& runtimeHost, CRITICAL_SECTION& mutex, @@ -17,7 +16,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge( RenderEffectCallback renderEffect, OutputReadyCallback outputReady, PaintCallback paint) : - mDeckLink(deckLink), + mVideoIO(videoIO), mRenderer(renderer), mRuntimeHost(runtimeHost), mMutex(mutex), @@ -29,7 +28,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge( { } -void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult completionResult) +void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult) { const auto now = std::chrono::steady_clock::now(); if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point()) @@ -44,11 +43,11 @@ void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult comp } mLastPlayoutCompletionTime = now; - if (completionResult == bmdOutputFrameDisplayedLate) + if (completionResult == VideoIOCompletionResult::DisplayedLate) ++mLateFrameCount; - else if (completionResult == bmdOutputFrameDropped) + else if (completionResult == VideoIOCompletionResult::Dropped) ++mDroppedFrameCount; - else if (completionResult == bmdOutputFrameFlushed) + else if (completionResult == VideoIOCompletionResult::Flushed) ++mFlushedFrameCount; mRuntimeHost.TrySetFramePacingStats( @@ -60,27 +59,15 @@ void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult comp mFlushedFrameCount); } -void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource) +void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame) { - mDeckLink.SetInputSourceMissing(hasNoInputSource); - mRuntimeHost.TrySetSignalStatus(!hasNoInputSource, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), mDeckLink.InputDisplayModeName()); + const VideoIOState& state = mVideoIO.State(); + mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName); - if (!mDeckLink.HasInputSource()) + if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr) return; // don't transfer texture when there's no input - long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight(); - IDeckLinkVideoBuffer* inputFrameBuffer = NULL; - void* videoPixels; - - if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK) - return; - - if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK) - { - inputFrameBuffer->Release(); - return; - } - inputFrameBuffer->GetBytes(&videoPixels); + const long textureSize = inputFrame.rowBytes * static_cast(inputFrame.height); EnterCriticalSection(&mMutex); @@ -89,14 +76,14 @@ void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFram glPixelStorei(GL_UNPACK_ALIGNMENT, 4); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer()); - glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, videoPixels, GL_DYNAMIC_DRAW); + glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, inputFrame.bytes, GL_DYNAMIC_DRAW); glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture()); // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data. - if (mDeckLink.InputPixelFormat() == VideoIOPixelFormat::V210) - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, NULL); + if (inputFrame.pixelFormat == VideoIOPixelFormat::V210) + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL); else - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL); glBindTexture(GL_TEXTURE_2D, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); @@ -104,21 +91,24 @@ void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFram wglMakeCurrent(NULL, NULL); LeaveCriticalSection(&mMutex); - - inputFrameBuffer->EndAccess(bmdBufferAccessRead); - inputFrameBuffer->Release(); } -void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult) +void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion) { - (void)completedFrame; - - RecordFramePacing(completionResult); + RecordFramePacing(completion.result); EnterCriticalSection(&mMutex); - // Get the first frame from the queue - IDeckLinkMutableVideoFrame* outputVideoFrame = mDeckLink.RotateOutputFrame(); + VideoIOOutputFrame outputFrame; + if (!mVideoIO.BeginOutputFrame(outputFrame)) + { + LeaveCriticalSection(&mMutex); + return; + } + const VideoIOState& state = mVideoIO.State(); + RenderPipelineFrameContext frameContext; + frameContext.videoState = state; + frameContext.completion = completion; // make GL context current in this thread wglMakeCurrent(mHdc, mHglrc); @@ -129,14 +119,14 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF mRenderEffect(); glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer()); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer()); - glBlitFramebuffer(0, 0, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), 0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR); glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer()); if (mOutputReady) mOutputReady(); - if (mDeckLink.OutputIsTenBit()) + if (state.outputPixelFormat == VideoIOPixelFormat::V210) { glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer()); - glViewport(0, 0, mDeckLink.OutputPackTextureWidth(), mDeckLink.OutputFrameHeight()); + glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height); glDisable(GL_SCISSOR_TEST); glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); @@ -147,9 +137,9 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution"); const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words"); if (outputResolutionLocation >= 0) - glUniform2f(outputResolutionLocation, static_cast(mDeckLink.OutputFrameWidth()), static_cast(mDeckLink.OutputFrameHeight())); + glUniform2f(outputResolutionLocation, static_cast(state.outputFrameSize.width), static_cast(state.outputFrameSize.height)); if (activeWordsLocation >= 0) - glUniform1f(activeWordsLocation, static_cast(ActiveV210WordsForWidth(mDeckLink.OutputFrameWidth()))); + glUniform1f(activeWordsLocation, static_cast(ActiveV210WordsForWidth(state.outputFrameSize.width))); glDrawArrays(GL_TRIANGLES, 0, 3); glUseProgram(0); glBindVertexArray(0); @@ -157,51 +147,31 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF } glFlush(); const auto renderEndTime = std::chrono::steady_clock::now(); - const double frameBudgetMilliseconds = mDeckLink.FrameBudgetMilliseconds(); + const double frameBudgetMilliseconds = state.frameBudgetMilliseconds; const double renderMilliseconds = std::chrono::duration_cast>(renderEndTime - renderStartTime).count(); mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds); mRuntimeHost.TryAdvanceFrame(); - IDeckLinkVideoBuffer* outputVideoFrameBuffer; - if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK) - { - wglMakeCurrent(NULL, NULL); - LeaveCriticalSection(&mMutex); - return; - } - - if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK) - { - outputVideoFrameBuffer->Release(); - wglMakeCurrent(NULL, NULL); - LeaveCriticalSection(&mMutex); - return; - } - - void* pFrame; - outputVideoFrameBuffer->GetBytes(&pFrame); - glPixelStorei(GL_PACK_ALIGNMENT, 4); glPixelStorei(GL_PACK_ROW_LENGTH, 0); - if (mDeckLink.OutputIsTenBit()) + if (state.outputPixelFormat == VideoIOPixelFormat::V210) { glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer()); - glReadPixels(0, 0, mDeckLink.OutputPackTextureWidth(), mDeckLink.OutputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, pFrame); + glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes); } else { glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer()); - glReadPixels(0, 0, mDeckLink.OutputFrameWidth(), mDeckLink.OutputFrameHeight(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, pFrame); + glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, outputFrame.bytes); } mPaint(); - outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); - outputVideoFrameBuffer->Release(); + mVideoIO.EndOutputFrame(outputFrame); - mDeckLink.AccountForCompletionResult(completionResult); + mVideoIO.AccountForCompletionResult(completion.result); // Schedule the next frame for playout - mDeckLink.ScheduleOutputFrame(outputVideoFrame); + mVideoIO.ScheduleOutputFrame(outputFrame); wglMakeCurrent(NULL, NULL); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.h similarity index 70% rename from apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.h rename to apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.h index 18827bf..a317b6b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLDeckLinkBridge.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLVideoIOBridge.h @@ -1,6 +1,6 @@ #pragma once -#include "DeckLinkAPI_h.h" +#include "VideoIOTypes.h" #include @@ -8,19 +8,24 @@ #include #include -class DeckLinkSession; class OpenGLRenderer; class RuntimeHost; -class OpenGLDeckLinkBridge +struct RenderPipelineFrameContext +{ + VideoIOState videoState; + VideoIOCompletion completion; +}; + +class OpenGLVideoIOBridge { public: using RenderEffectCallback = std::function; using OutputReadyCallback = std::function; using PaintCallback = std::function; - OpenGLDeckLinkBridge( - DeckLinkSession& deckLink, + OpenGLVideoIOBridge( + VideoIODevice& videoIO, OpenGLRenderer& renderer, RuntimeHost& runtimeHost, CRITICAL_SECTION& mutex, @@ -30,13 +35,13 @@ public: OutputReadyCallback outputReady, PaintCallback paint); - void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource); - void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult); + void VideoFrameArrived(const VideoIOFrame& inputFrame); + void PlayoutFrameCompleted(const VideoIOCompletion& completion); private: - void RecordFramePacing(BMDOutputFrameCompletionResult completionResult); + void RecordFramePacing(VideoIOCompletionResult completionResult); - DeckLinkSession& mDeckLink; + VideoIODevice& mVideoIO; OpenGLRenderer& mRenderer; RuntimeHost& mRuntimeHost; CRITICAL_SECTION& mMutex; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp index 381555f..434dd31 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp @@ -1213,8 +1213,16 @@ void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) +{ + SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable, + externalKeyingRequested, externalKeyingActive, statusMessage); +} + +void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, + bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) { std::lock_guard lock(mMutex); + mDeckLinkOutputStatus.backendName = backendName; mDeckLinkOutputStatus.modelName = modelName; mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying; mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying; @@ -1889,6 +1897,17 @@ JsonValue RuntimeHost::BuildStateValue() const deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage)); root.set("decklink", deckLink); + JsonValue videoIO = JsonValue::MakeObject(); + videoIO.set("backend", JsonValue(mDeckLinkOutputStatus.backendName)); + videoIO.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName)); + videoIO.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying)); + videoIO.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying)); + videoIO.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable)); + videoIO.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested)); + videoIO.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive)); + videoIO.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage)); + root.set("videoIO", videoIO); + JsonValue performance = JsonValue::MakeObject(); performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds)); performance.set("renderMs", JsonValue(mRenderMilliseconds)); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h index 5dc7779..b517cac 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h @@ -39,6 +39,8 @@ public: bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage); + void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, + bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage); void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, @@ -86,6 +88,7 @@ private: struct DeckLinkOutputStatus { + std::string backendName = "decklink"; std::string modelName; bool supportsInternalKeying = false; bool supportsExternalKeying = false; diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.cpp similarity index 85% rename from apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.cpp rename to apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.cpp index fb62df4..8538b1c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.cpp @@ -50,7 +50,16 @@ uint16_t Component(uint32_t word, unsigned index) const char* VideoIOPixelFormatName(VideoIOPixelFormat format) { - return format == VideoIOPixelFormat::V210 ? "10-bit YUV v210" : "8-bit YUV UYVY"; + switch (format) + { + case VideoIOPixelFormat::V210: + return "10-bit YUV v210"; + case VideoIOPixelFormat::Bgra8: + return "8-bit BGRA"; + case VideoIOPixelFormat::Uyvy8: + default: + return "8-bit YUV UYVY"; + } } bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format) @@ -58,21 +67,32 @@ bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format) return format == VideoIOPixelFormat::V210; } -BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format) -{ - return format == VideoIOPixelFormat::V210 ? bmdFormat10BitYUV : bmdFormat8BitYUV; -} - -VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format) -{ - return format == bmdFormat10BitYUV ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8; -} - VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported) { return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8; } +unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format) +{ + switch (format) + { + case VideoIOPixelFormat::Uyvy8: + return 2u; + case VideoIOPixelFormat::Bgra8: + return 4u; + case VideoIOPixelFormat::V210: + default: + return 0u; + } +} + +unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth) +{ + if (format == VideoIOPixelFormat::V210) + return MinimumV210RowBytes(frameWidth); + return frameWidth * VideoIOBytesPerPixel(format); +} + unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes) { return (rowBytes + 3u) / 4u; diff --git a/apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.h similarity index 83% rename from apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.h rename to apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.h index fd49f30..42b89ce 100644 --- a/apps/LoopThroughWithOpenGLCompositing/decklink/VideoIOFormat.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOFormat.h @@ -1,14 +1,13 @@ #pragma once -#include "DeckLinkAPI_h.h" - #include #include enum class VideoIOPixelFormat { Uyvy8, - V210 + V210, + Bgra8 }; struct V210CodeValues @@ -27,9 +26,9 @@ struct V210SixPixelBlock const char* VideoIOPixelFormatName(VideoIOPixelFormat format); bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format); -BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format); -VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format); VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported); +unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format); +unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth); unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes); unsigned MinimumV210RowBytes(unsigned frameWidth); unsigned ActiveV210WordsForWidth(unsigned frameWidth); diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h new file mode 100644 index 0000000..6ce9b21 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h @@ -0,0 +1,137 @@ +#pragma once + +#include "DeckLinkDisplayMode.h" +#include "VideoIOFormat.h" + +#include +#include +#include + +enum class VideoIOBackend +{ + DeckLink +}; + +enum class VideoIOCompletionResult +{ + Completed, + DisplayedLate, + Dropped, + Flushed, + Unknown +}; + +struct VideoIOConfig +{ + VideoFormatSelection videoModes; + bool externalKeyingEnabled = false; + bool preferTenBit = true; +}; + +struct VideoIOState +{ + FrameSize inputFrameSize; + FrameSize outputFrameSize; + VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8; + VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Bgra8; + unsigned inputFrameRowBytes = 0; + unsigned outputFrameRowBytes = 0; + unsigned captureTextureWidth = 0; + unsigned outputPackTextureWidth = 0; + std::string inputDisplayModeName = "1080p59.94"; + std::string outputDisplayModeName = "1080p59.94"; + std::string outputModelName; + std::string statusMessage; + std::string formatStatusMessage; + bool hasInputDevice = false; + bool hasInputSource = false; + bool supportsInternalKeying = false; + bool supportsExternalKeying = false; + bool keyerInterfaceAvailable = false; + bool externalKeyingActive = false; + double frameBudgetMilliseconds = 0.0; +}; + +struct VideoIOFrame +{ + void* bytes = nullptr; + long rowBytes = 0; + unsigned width = 0; + unsigned height = 0; + VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Uyvy8; + bool hasNoInputSource = false; +}; + +struct VideoIOOutputFrame +{ + void* bytes = nullptr; + long rowBytes = 0; + unsigned width = 0; + unsigned height = 0; + VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8; + void* nativeFrame = nullptr; + void* nativeBuffer = nullptr; +}; + +struct VideoIOCompletion +{ + VideoIOCompletionResult result = VideoIOCompletionResult::Completed; +}; + +struct VideoIOScheduleTime +{ + int64_t streamTime = 0; + int64_t duration = 0; + int64_t timeScale = 0; + uint64_t frameIndex = 0; +}; + +class VideoIODevice +{ +public: + using InputFrameCallback = std::function; + using OutputFrameCallback = std::function; + + virtual ~VideoIODevice() = default; + virtual void ReleaseResources() = 0; + virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0; + virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) = 0; + virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0; + virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0; + virtual bool Start() = 0; + virtual bool Stop() = 0; + virtual const VideoIOState& State() const = 0; + virtual VideoIOState& MutableState() = 0; + virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0; + virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0; + virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0; + virtual void AccountForCompletionResult(VideoIOCompletionResult result) = 0; + + bool HasInputDevice() const { return State().hasInputDevice; } + bool HasInputSource() const { return State().hasInputSource; } + bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; } + const FrameSize& InputFrameSize() const { return State().inputFrameSize; } + const FrameSize& OutputFrameSize() const { return State().outputFrameSize; } + unsigned InputFrameWidth() const { return State().inputFrameSize.width; } + unsigned InputFrameHeight() const { return State().inputFrameSize.height; } + unsigned OutputFrameWidth() const { return State().outputFrameSize.width; } + unsigned OutputFrameHeight() const { return State().outputFrameSize.height; } + VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; } + VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; } + bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); } + bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); } + unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; } + unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; } + unsigned CaptureTextureWidth() const { return State().captureTextureWidth; } + unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; } + const std::string& FormatStatusMessage() const { return State().formatStatusMessage; } + const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; } + const std::string& OutputModelName() const { return State().outputModelName; } + bool SupportsInternalKeying() const { return State().supportsInternalKeying; } + bool SupportsExternalKeying() const { return State().supportsExternalKeying; } + bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; } + bool ExternalKeyingActive() const { return State().externalKeyingActive; } + const std::string& StatusMessage() const { return State().statusMessage; } + double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; } + void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; } +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp new file mode 100644 index 0000000..e8068ed --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp @@ -0,0 +1,37 @@ +#include "VideoPlayoutScheduler.h" + +void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale) +{ + mFrameDuration = frameDuration; + mTimeScale = timeScale; + Reset(); +} + +void VideoPlayoutScheduler::Reset() +{ + mScheduledFrameIndex = 0; +} + +VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime() +{ + VideoIOScheduleTime time; + time.streamTime = static_cast(mScheduledFrameIndex) * mFrameDuration; + time.duration = mFrameDuration; + time.timeScale = mTimeScale; + time.frameIndex = mScheduledFrameIndex; + ++mScheduledFrameIndex; + return time; +} + +void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result) +{ + if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped) + mScheduledFrameIndex += 2; +} + +double VideoPlayoutScheduler::FrameBudgetMilliseconds() const +{ + return mTimeScale != 0 + ? (static_cast(mFrameDuration) * 1000.0) / static_cast(mTimeScale) + : 0.0; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h new file mode 100644 index 0000000..984f606 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h @@ -0,0 +1,22 @@ +#pragma once + +#include "VideoIOTypes.h" + +#include + +class VideoPlayoutScheduler +{ +public: + void Configure(int64_t frameDuration, int64_t timeScale); + void Reset(); + VideoIOScheduleTime NextScheduleTime(); + void AccountForCompletionResult(VideoIOCompletionResult result); + double FrameBudgetMilliseconds() const; + uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; } + int64_t TimeScale() const { return mTimeScale; } + +private: + int64_t mFrameDuration = 0; + int64_t mTimeScale = 0; + uint64_t mScheduledFrameIndex = 0; +}; diff --git a/tests/VideoIODeviceFakeTests.cpp b/tests/VideoIODeviceFakeTests.cpp new file mode 100644 index 0000000..4107c96 --- /dev/null +++ b/tests/VideoIODeviceFakeTests.cpp @@ -0,0 +1,151 @@ +#include "VideoIOTypes.h" + +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +class FakeVideoIODevice : public VideoIODevice +{ +public: + void ReleaseResources() override {} + + bool DiscoverDevicesAndModes(const VideoFormatSelection&, std::string&) override + { + mState.inputFrameSize = { 1920, 1080 }; + mState.outputFrameSize = { 1920, 1080 }; + mState.inputDisplayModeName = "fake 1080p"; + mState.outputModelName = "Fake Video IO"; + mState.hasInputDevice = true; + return true; + } + + bool SelectPreferredFormats(const VideoFormatSelection&, std::string&) override + { + mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8; + mState.outputPixelFormat = VideoIOPixelFormat::Bgra8; + mState.inputFrameRowBytes = VideoIORowBytes(mState.inputPixelFormat, mState.inputFrameSize.width); + mState.outputFrameRowBytes = VideoIORowBytes(mState.outputPixelFormat, mState.outputFrameSize.width); + mState.captureTextureWidth = PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes); + mState.outputPackTextureWidth = mState.outputFrameSize.width; + return true; + } + + bool ConfigureInput(InputFrameCallback callback, const VideoFormat&, std::string&) override + { + mInputCallback = callback; + return true; + } + + bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat&, bool, std::string&) override + { + mOutputCallback = callback; + return true; + } + + bool Start() override + { + mState.hasInputSource = true; + VideoIOFrame input; + input.bytes = mInputBytes.data(); + input.rowBytes = static_cast(mState.inputFrameRowBytes); + input.width = mState.inputFrameSize.width; + input.height = mState.inputFrameSize.height; + input.pixelFormat = mState.inputPixelFormat; + if (mInputCallback) + mInputCallback(input); + if (mOutputCallback) + mOutputCallback(VideoIOCompletion{ VideoIOCompletionResult::Completed }); + return true; + } + + bool Stop() override { return true; } + const VideoIOState& State() const override { return mState; } + VideoIOState& MutableState() override { return mState; } + + bool BeginOutputFrame(VideoIOOutputFrame& frame) override + { + frame.bytes = mOutputBytes.data(); + frame.rowBytes = static_cast(mState.outputFrameRowBytes); + frame.width = mState.outputFrameSize.width; + frame.height = mState.outputFrameSize.height; + frame.pixelFormat = mState.outputPixelFormat; + return true; + } + + void EndOutputFrame(VideoIOOutputFrame&) override {} + + bool ScheduleOutputFrame(const VideoIOOutputFrame&) override + { + ++mScheduledFrames; + return true; + } + + void AccountForCompletionResult(VideoIOCompletionResult result) override + { + mLastCompletion = result; + } + + unsigned ScheduledFrames() const { return mScheduledFrames; } + VideoIOCompletionResult LastCompletion() const { return mLastCompletion; } + +private: + VideoIOState mState; + InputFrameCallback mInputCallback; + OutputFrameCallback mOutputCallback; + std::array mInputBytes = {}; + std::array mOutputBytes = {}; + unsigned mScheduledFrames = 0; + VideoIOCompletionResult mLastCompletion = VideoIOCompletionResult::Unknown; +}; +} + +int main() +{ + FakeVideoIODevice device; + VideoFormatSelection selection; + std::string error; + bool inputSeen = false; + bool outputSeen = false; + + Expect(device.DiscoverDevicesAndModes(selection, error), "fake discovery succeeds"); + Expect(device.SelectPreferredFormats(selection, error), "fake format selection succeeds"); + Expect(device.ConfigureInput([&](const VideoIOFrame& frame) { + inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8; + }, selection.input, error), "fake input config succeeds"); + Expect(device.ConfigureOutput([&](const VideoIOCompletion& completion) { + outputSeen = completion.result == VideoIOCompletionResult::Completed; + }, selection.output, false, error), "fake output config succeeds"); + Expect(device.Start(), "fake device starts"); + + VideoIOOutputFrame outputFrame; + Expect(device.BeginOutputFrame(outputFrame), "fake output frame can be acquired"); + device.EndOutputFrame(outputFrame); + device.AccountForCompletionResult(VideoIOCompletionResult::Completed); + Expect(device.ScheduleOutputFrame(outputFrame), "fake output frame can be scheduled"); + + Expect(inputSeen, "fake input callback emits generic frame"); + Expect(outputSeen, "fake output callback emits generic completion"); + Expect(device.ScheduledFrames() == 1, "fake backend schedules one frame"); + Expect(device.LastCompletion() == VideoIOCompletionResult::Completed, "fake backend records generic completion"); + + if (gFailures != 0) + { + std::cerr << gFailures << " VideoIODevice fake test failure(s).\n"; + return 1; + } + + std::cout << "VideoIODevice fake tests passed.\n"; + return 0; +} diff --git a/tests/VideoIOFormatTests.cpp b/tests/VideoIOFormatTests.cpp index 3216bf0..751e466 100644 --- a/tests/VideoIOFormatTests.cpp +++ b/tests/VideoIOFormatTests.cpp @@ -1,4 +1,5 @@ #include "VideoIOFormat.h" +#include "DeckLinkVideoIOFormat.h" #include #include @@ -22,6 +23,7 @@ void TestPreferredFormatSelection() Expect(ChoosePreferredVideoIOFormat(false) == VideoIOPixelFormat::Uyvy8, "8-bit is used as fallback"); Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::V210) == bmdFormat10BitYUV, "v210 maps to DeckLink 10-bit YUV"); Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Uyvy8) == bmdFormat8BitYUV, "UYVY maps to DeckLink 8-bit YUV"); + Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Bgra8) == bmdFormat8BitBGRA, "BGRA maps to DeckLink 8-bit BGRA"); } void TestRowByteHelpers() @@ -31,6 +33,9 @@ void TestRowByteHelpers() Expect(MinimumV210RowBytes(3840) == 10240, "3840-wide v210 active row bytes"); Expect(PackedTextureWidthFromRowBytes(5120) == 1280, "packed texture width is row bytes divided into RGBA byte texels"); Expect(ActiveV210WordsForWidth(1920) == 1280, "active v210 words match 1920 width"); + Expect(VideoIORowBytes(VideoIOPixelFormat::Uyvy8, 1920) == 3840, "UYVY row bytes"); + Expect(VideoIORowBytes(VideoIOPixelFormat::Bgra8, 1920) == 7680, "BGRA row bytes"); + Expect(VideoIORowBytes(VideoIOPixelFormat::V210, 1920) == 5120, "v210 row bytes"); } void TestV210PackUnpack() diff --git a/tests/VideoPlayoutSchedulerTests.cpp b/tests/VideoPlayoutSchedulerTests.cpp new file mode 100644 index 0000000..28b2b13 --- /dev/null +++ b/tests/VideoPlayoutSchedulerTests.cpp @@ -0,0 +1,81 @@ +#include "VideoPlayoutScheduler.h" + +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +void ExpectNear(double actual, double expected, double tolerance, const char* message) +{ + Expect(std::fabs(actual - expected) <= tolerance, message); +} + +void TestScheduleAdvancesFromZero() +{ + VideoPlayoutScheduler scheduler; + scheduler.Configure(1001, 60000); + + const VideoIOScheduleTime first = scheduler.NextScheduleTime(); + const VideoIOScheduleTime second = scheduler.NextScheduleTime(); + const VideoIOScheduleTime third = scheduler.NextScheduleTime(); + + Expect(first.streamTime == 0, "first frame starts at stream time zero"); + Expect(first.duration == 1001, "duration is preserved"); + Expect(first.timeScale == 60000, "time scale is preserved"); + Expect(second.streamTime == 1001, "second frame advances by one duration"); + Expect(third.streamTime == 2002, "third frame advances by two durations"); +} + +void TestLateAndDroppedSkipAhead() +{ + VideoPlayoutScheduler scheduler; + scheduler.Configure(1000, 50000); + + (void)scheduler.NextScheduleTime(); + scheduler.AccountForCompletionResult(VideoIOCompletionResult::DisplayedLate); + Expect(scheduler.NextScheduleTime().streamTime == 3000, "late completion preserves the existing two-frame skip policy"); + + scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped); + Expect(scheduler.NextScheduleTime().streamTime == 6000, "dropped completion preserves the existing two-frame skip policy"); +} + +void TestFrameBudgets() +{ + VideoPlayoutScheduler scheduler; + scheduler.Configure(1000, 50000); + ExpectNear(scheduler.FrameBudgetMilliseconds(), 20.0, 0.0001, "50 fps budget"); + + scheduler.Configure(1001, 60000); + ExpectNear(scheduler.FrameBudgetMilliseconds(), 16.6833, 0.0001, "59.94 fps budget"); + + scheduler.Configure(1, 60); + ExpectNear(scheduler.FrameBudgetMilliseconds(), 16.6667, 0.0001, "60 fps budget"); +} +} + +int main() +{ + TestScheduleAdvancesFromZero(); + TestLateAndDroppedSkipAhead(); + TestFrameBudgets(); + + if (gFailures != 0) + { + std::cerr << gFailures << " VideoPlayoutScheduler test failure(s).\n"; + return 1; + } + + std::cout << "VideoPlayoutScheduler tests passed.\n"; + return 0; +}