Decklink abstraction
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m41s
CI / Windows Release Package (push) Successful in 2m20s

This commit is contained in:
2026-05-08 16:27:40 +10:00
parent 6d5a606107
commit ebbc11bb34
23 changed files with 971 additions and 342 deletions

View File

@@ -46,8 +46,8 @@ set(APP_SOURCES
"${APP_DIR}/decklink/DeckLinkFrameTransfer.h" "${APP_DIR}/decklink/DeckLinkFrameTransfer.h"
"${APP_DIR}/decklink/DeckLinkSession.cpp" "${APP_DIR}/decklink/DeckLinkSession.cpp"
"${APP_DIR}/decklink/DeckLinkSession.h" "${APP_DIR}/decklink/DeckLinkSession.h"
"${APP_DIR}/decklink/VideoIOFormat.cpp" "${APP_DIR}/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/decklink/VideoIOFormat.h" "${APP_DIR}/decklink/DeckLinkVideoIOFormat.h"
"${APP_DIR}/gl/GLExtensions.cpp" "${APP_DIR}/gl/GLExtensions.cpp"
"${APP_DIR}/gl/GLExtensions.h" "${APP_DIR}/gl/GLExtensions.h"
"${APP_DIR}/gl/GlobalParamsBuffer.cpp" "${APP_DIR}/gl/GlobalParamsBuffer.cpp"
@@ -59,12 +59,12 @@ set(APP_SOURCES
"${APP_DIR}/gl/OpenGLComposite.cpp" "${APP_DIR}/gl/OpenGLComposite.cpp"
"${APP_DIR}/gl/OpenGLComposite.h" "${APP_DIR}/gl/OpenGLComposite.h"
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp" "${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
"${APP_DIR}/gl/OpenGLDeckLinkBridge.cpp"
"${APP_DIR}/gl/OpenGLDeckLinkBridge.h"
"${APP_DIR}/gl/OpenGLRenderPass.cpp" "${APP_DIR}/gl/OpenGLRenderPass.cpp"
"${APP_DIR}/gl/OpenGLRenderPass.h" "${APP_DIR}/gl/OpenGLRenderPass.h"
"${APP_DIR}/gl/OpenGLRenderer.cpp" "${APP_DIR}/gl/OpenGLRenderer.cpp"
"${APP_DIR}/gl/OpenGLRenderer.h" "${APP_DIR}/gl/OpenGLRenderer.h"
"${APP_DIR}/gl/OpenGLVideoIOBridge.cpp"
"${APP_DIR}/gl/OpenGLVideoIOBridge.h"
"${APP_DIR}/gl/OpenGLShaderPrograms.cpp" "${APP_DIR}/gl/OpenGLShaderPrograms.cpp"
"${APP_DIR}/gl/OpenGLShaderPrograms.h" "${APP_DIR}/gl/OpenGLShaderPrograms.h"
"${APP_DIR}/gl/PngScreenshotWriter.cpp" "${APP_DIR}/gl/PngScreenshotWriter.cpp"
@@ -104,6 +104,11 @@ set(APP_SOURCES
"${APP_DIR}/stdafx.cpp" "${APP_DIR}/stdafx.cpp"
"${APP_DIR}/stdafx.h" "${APP_DIR}/stdafx.h"
"${APP_DIR}/targetver.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}) add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
@@ -116,6 +121,7 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${APP_DIR}/platform" "${APP_DIR}/platform"
"${APP_DIR}/runtime" "${APP_DIR}/runtime"
"${APP_DIR}/shader" "${APP_DIR}/shader"
"${APP_DIR}/videoio"
) )
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
@@ -244,13 +250,15 @@ endif()
add_test(NAME OscServerTests COMMAND OscServerTests) add_test(NAME OscServerTests COMMAND OscServerTests)
add_executable(VideoIOFormatTests 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" "${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
) )
target_include_directories(VideoIOFormatTests PRIVATE target_include_directories(VideoIOFormatTests PRIVATE
"${APP_DIR}" "${APP_DIR}"
"${APP_DIR}/decklink" "${APP_DIR}/decklink"
"${APP_DIR}/videoio"
) )
if(MSVC) if(MSVC)
@@ -259,6 +267,40 @@ endif()
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests) 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 install(TARGETS LoopThroughWithOpenGLCompositing
RUNTIME DESTINATION "." RUNTIME DESTINATION "."
) )

View File

@@ -89,7 +89,7 @@
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile> <ClCompile>
<Optimization>Disabled</Optimization> <Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks> <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary> <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
@@ -111,7 +111,7 @@
</Midl> </Midl>
<ClCompile> <ClCompile>
<Optimization>Disabled</Optimization> <Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks> <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary> <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
@@ -131,7 +131,7 @@
<ClCompile> <ClCompile>
<Optimization>MaxSpeed</Optimization> <Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking> <FunctionLevelLinking>true</FunctionLevelLinking>
@@ -156,7 +156,7 @@
<ClCompile> <ClCompile>
<Optimization>MaxSpeed</Optimization> <Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;decklink;gl;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalIncludeDirectories>.;control;decklink;gl;videoio;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking> <FunctionLevelLinking>true</FunctionLevelLinking>
@@ -184,6 +184,7 @@
<ClCompile Include="gl\PngScreenshotWriter.cpp" /> <ClCompile Include="gl\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\ShaderBuildQueue.cpp" /> <ClCompile Include="gl\ShaderBuildQueue.cpp" />
<ClCompile Include="gl\TemporalHistoryBuffers.cpp" /> <ClCompile Include="gl\TemporalHistoryBuffers.cpp" />
<ClCompile Include="gl\OpenGLVideoIOBridge.cpp" />
<ClCompile Include="stdafx.cpp"> <ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
@@ -193,8 +194,10 @@
<ClCompile Include="DeckLinkAPI_i.c" /> <ClCompile Include="DeckLinkAPI_i.c" />
<ClCompile Include="control\RuntimeServices.cpp" /> <ClCompile Include="control\RuntimeServices.cpp" />
<ClCompile Include="decklink\DeckLinkSession.cpp" /> <ClCompile Include="decklink\DeckLinkSession.cpp" />
<ClCompile Include="decklink\VideoIOFormat.cpp" /> <ClCompile Include="decklink\DeckLinkVideoIOFormat.cpp" />
<ClCompile Include="runtime\RuntimeClock.cpp" /> <ClCompile Include="runtime\RuntimeClock.cpp" />
<ClCompile Include="videoio\VideoIOFormat.cpp" />
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="gl\GLExtensions.h" /> <ClInclude Include="gl\GLExtensions.h" />
@@ -206,13 +209,17 @@
<ClInclude Include="gl\PngScreenshotWriter.h" /> <ClInclude Include="gl\PngScreenshotWriter.h" />
<ClInclude Include="gl\ShaderBuildQueue.h" /> <ClInclude Include="gl\ShaderBuildQueue.h" />
<ClInclude Include="gl\TemporalHistoryBuffers.h" /> <ClInclude Include="gl\TemporalHistoryBuffers.h" />
<ClInclude Include="gl\OpenGLVideoIOBridge.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="stdafx.h" /> <ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" /> <ClInclude Include="targetver.h" />
<ClInclude Include="control\RuntimeServices.h" /> <ClInclude Include="control\RuntimeServices.h" />
<ClInclude Include="decklink\DeckLinkSession.h" /> <ClInclude Include="decklink\DeckLinkSession.h" />
<ClInclude Include="decklink\VideoIOFormat.h" /> <ClInclude Include="decklink\DeckLinkVideoIOFormat.h" />
<ClInclude Include="runtime\RuntimeClock.h" /> <ClInclude Include="runtime\RuntimeClock.h" />
<ClInclude Include="videoio\VideoIOFormat.h" />
<ClInclude Include="videoio\VideoIOTypes.h" />
<ClInclude Include="videoio\VideoPlayoutScheduler.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Image Include="LoopThroughWithOpenGLCompositing.ico" /> <Image Include="LoopThroughWithOpenGLCompositing.ico" />

View File

@@ -45,6 +45,9 @@
<ClCompile Include="gl\TemporalHistoryBuffers.cpp"> <ClCompile Include="gl\TemporalHistoryBuffers.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="gl\OpenGLVideoIOBridge.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="stdafx.cpp"> <ClCompile Include="stdafx.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
@@ -57,12 +60,18 @@
<ClCompile Include="decklink\DeckLinkSession.cpp"> <ClCompile Include="decklink\DeckLinkSession.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="decklink\VideoIOFormat.cpp"> <ClCompile Include="decklink\DeckLinkVideoIOFormat.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="runtime\RuntimeClock.cpp"> <ClCompile Include="runtime\RuntimeClock.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="videoio\VideoIOFormat.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="gl\GLExtensions.h"> <ClInclude Include="gl\GLExtensions.h">
@@ -92,6 +101,9 @@
<ClInclude Include="gl\TemporalHistoryBuffers.h"> <ClInclude Include="gl\TemporalHistoryBuffers.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="gl\OpenGLVideoIOBridge.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h"> <ClInclude Include="resource.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
@@ -107,12 +119,21 @@
<ClInclude Include="decklink\DeckLinkSession.h"> <ClInclude Include="decklink\DeckLinkSession.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="decklink\VideoIOFormat.h"> <ClInclude Include="decklink\DeckLinkVideoIOFormat.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="runtime\RuntimeClock.h"> <ClInclude Include="runtime\RuntimeClock.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="videoio\VideoIOFormat.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\VideoIOTypes.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\VideoPlayoutScheduler.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Image Include="LoopThroughWithOpenGLCompositing.ico"> <Image Include="LoopThroughWithOpenGLCompositing.ico">

View File

@@ -1,11 +1,11 @@
#include "DeckLinkFrameTransfer.h" #include "DeckLinkFrameTransfer.h"
#include "OpenGLComposite.h" #include "DeckLinkSession.h"
//////////////////////////////////////////// ////////////////////////////////////////////
// DeckLink Capture Delegate Class // DeckLink Capture Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
CaptureDelegate::CaptureDelegate(OpenGLComposite* pOwner) : CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner), m_pOwner(pOwner),
mRefCount(1) mRefCount(1)
{ {
@@ -39,7 +39,7 @@ HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputF
} }
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource; bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
m_pOwner->VideoFrameArrived(inputFrame, hasNoInputSource); m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
return S_OK; return S_OK;
} }
@@ -51,7 +51,7 @@ HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvent
//////////////////////////////////////////// ////////////////////////////////////////////
// DeckLink Playout Delegate Class // DeckLink Playout Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
PlayoutDelegate::PlayoutDelegate(OpenGLComposite* pOwner) : PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner), m_pOwner(pOwner),
mRefCount(1) mRefCount(1)
{ {
@@ -94,7 +94,7 @@ HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedF
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n"); OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
} }
m_pOwner->PlayoutFrameCompleted(completedFrame, result); m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
return S_OK; return S_OK;
} }

View File

@@ -6,18 +6,18 @@
#include "DeckLinkAPI_h.h" #include "DeckLinkAPI_h.h"
class OpenGLComposite; class DeckLinkSession;
//////////////////////////////////////////// ////////////////////////////////////////////
// Capture Delegate Class // Capture Delegate Class
//////////////////////////////////////////// ////////////////////////////////////////////
class CaptureDelegate : public IDeckLinkInputCallback class CaptureDelegate : public IDeckLinkInputCallback
{ {
OpenGLComposite* m_pOwner; DeckLinkSession* m_pOwner;
LONG mRefCount; LONG mRefCount;
public: public:
CaptureDelegate(OpenGLComposite* pOwner); CaptureDelegate(DeckLinkSession* pOwner);
// IUnknown needs only a dummy implementation // IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);
@@ -33,11 +33,11 @@ public:
//////////////////////////////////////////// ////////////////////////////////////////////
class PlayoutDelegate : public IDeckLinkVideoOutputCallback class PlayoutDelegate : public IDeckLinkVideoOutputCallback
{ {
OpenGLComposite* m_pOwner; DeckLinkSession* m_pOwner;
LONG mRefCount; LONG mRefCount;
public: public:
PlayoutDelegate(OpenGLComposite* pOwner); PlayoutDelegate(DeckLinkSession* pOwner);
// IUnknown needs only a dummy implementation // IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv); virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv);

View File

@@ -7,6 +7,7 @@
#include <cstring> #include <cstring>
#include <new> #include <new>
#include <sstream> #include <sstream>
#include <utility>
#include <vector> #include <vector>
namespace namespace
@@ -82,7 +83,7 @@ void DeckLinkSession::ReleaseResources()
if (keyer != nullptr) if (keyer != nullptr)
{ {
keyer->Disable(); keyer->Disable();
externalKeyingActive = false; mState.externalKeyingActive = false;
} }
keyer.Release(); keyer.Release();
@@ -97,8 +98,8 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
CComPtr<IDeckLinkDisplayMode> inputMode; CComPtr<IDeckLinkDisplayMode> inputMode;
CComPtr<IDeckLinkDisplayMode> outputMode; CComPtr<IDeckLinkDisplayMode> outputMode;
inputDisplayModeName = videoModes.input.displayName; mState.inputDisplayModeName = videoModes.input.displayName;
outputDisplayModeName = videoModes.output.displayName; mState.outputDisplayModeName = videoModes.output.displayName;
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator)); HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
if (FAILED(result)) if (FAILED(result))
@@ -150,9 +151,9 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
output.Release(); output.Release();
else else
{ {
outputModelName = modelName; mState.outputModelName = modelName;
supportsInternalKeying = deviceSupportsInternalKeying; mState.supportsInternalKeying = deviceSupportsInternalKeying;
supportsExternalKeying = deviceSupportsExternalKeying; mState.supportsExternalKeying = deviceSupportsExternalKeying;
} }
} }
@@ -200,18 +201,24 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
return false; return false;
} }
outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) }; mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
inputFrameSize = inputMode mState.inputFrameSize = inputMode
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) } ? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
: outputFrameSize; : mState.outputFrameSize;
if (!input) if (!input)
inputDisplayModeName = "No input - black frame"; mState.inputDisplayModeName = "No input - black frame";
BMDTimeValue frameDuration = 0;
BMDTimeScale frameTimescale = 0;
outputMode->GetFrameRate(&frameDuration, &frameTimescale); outputMode->GetFrameRate(&frameDuration, &frameTimescale);
mScheduler.Configure(frameDuration, frameTimescale);
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
inputFrameRowBytes = inputFrameSize.width * 2u; mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
outputFrameRowBytes = outputFrameSize.width * 4u; mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
captureTextureWidth = inputFrameSize.width / 2u; mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
outputPackTextureWidth = outputFrameSize.width; mState.outputPackTextureWidth = mState.outputFrameSize.width;
mState.hasInputDevice = input != nullptr;
mState.hasInputSource = false;
return true; return true;
} }
@@ -224,95 +231,95 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
return false; return false;
} }
formatStatusMessage.clear(); mState.formatStatusMessage.clear();
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV); 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) 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); const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
outputPixelFormat = ChoosePreferredVideoIOFormat(outputTenBitSupported); mState.outputPixelFormat = outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8;
if (!outputTenBitSupported) 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; 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."; error = "DeckLink output setup failed while calculating output row bytes.";
return false; return false;
} }
outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes); mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
outputPackTextureWidth = OutputIsTenBit() mState.outputPackTextureWidth = OutputIsTenBit()
? PackedTextureWidthFromRowBytes(outputFrameRowBytes) ? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
: outputFrameSize.width; : mState.outputFrameSize.width;
if (InputIsTenBit()) if (InputIsTenBit())
{ {
int deckLinkInputRowBytes = 0; int deckLinkInputRowBytes = 0;
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, inputFrameSize.width, &deckLinkInputRowBytes) == S_OK) if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes); mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
else else
inputFrameRowBytes = MinimumV210RowBytes(inputFrameSize.width); mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
} }
else else
{ {
inputFrameRowBytes = inputFrameSize.width * 2u; mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
} }
captureTextureWidth = InputIsTenBit() mState.captureTextureWidth = InputIsTenBit()
? PackedTextureWidthFromRowBytes(inputFrameRowBytes) ? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
: inputFrameSize.width / 2u; : mState.inputFrameSize.width / 2u;
std::ostringstream status; std::ostringstream status;
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(inputPixelFormat) : "none") status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
<< ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA") << "."; << ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
if (!formatStatusMessage.empty()) if (!mState.formatStatusMessage.empty())
status << " " << formatStatusMessage; status << " " << mState.formatStatusMessage;
formatStatusMessage = status.str(); mState.formatStatusMessage = status.str();
return true; 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; mInputFrameCallback = std::move(callback);
(void)hglrc;
if (!input) if (!input)
{ {
hasNoInputSource = true; mState.hasInputSource = false;
inputDisplayModeName = "No input - black frame"; mState.inputDisplayModeName = "No input - black frame";
return true; return true;
} }
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(inputPixelFormat); const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK) 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"); OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
inputPixelFormat = VideoIOPixelFormat::Uyvy8; mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
inputFrameRowBytes = inputFrameSize.width * 2u; mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
captureTextureWidth = inputFrameSize.width / 2u; mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK) if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
{ {
std::ostringstream status; std::ostringstream status;
status << "DeckLink formats: capture " << VideoIOPixelFormatName(inputPixelFormat) status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
<< ", output " << (OutputIsTenBit() ? "10-bit YUV v210" : "8-bit BGRA") << ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
<< ". DeckLink 10-bit input enable failed; using 8-bit capture."; << ". DeckLink 10-bit input enable failed; using 8-bit capture.";
formatStatusMessage = status.str(); mState.formatStatusMessage = status.str();
goto input_enabled; goto input_enabled;
} }
} }
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n"); OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
input.Release(); input.Release();
hasNoInputSource = true; mState.hasInputDevice = false;
inputDisplayModeName = "No input - black frame"; mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true; return true;
} }
input_enabled: input_enabled:
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(owner)); captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
if (captureDelegate == nullptr) if (captureDelegate == nullptr)
{ {
error = "DeckLink input setup failed while creating the capture callback."; error = "DeckLink input setup failed while creating the capture callback.";
@@ -327,10 +334,9 @@ input_enabled:
return true; 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; mOutputFrameCallback = std::move(callback);
(void)hglrc;
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK) 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) if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
keyerInterfaceAvailable = true; mState.keyerInterfaceAvailable = true;
if (externalKeyingEnabled) 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) 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 else
{ {
externalKeyingActive = true; mState.externalKeyingActive = true;
statusMessage = "External keying is active on the selected DeckLink output."; 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++) for (int i = 0; i < 10; i++)
{ {
CComPtr<IDeckLinkMutableVideoFrame> outputFrame; CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
const BMDPixelFormat deckLinkOutputPixelFormat = OutputIsTenBit() ? bmdFormat10BitYUV : bmdFormat8BitBGRA; const BMDPixelFormat deckLinkOutputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat);
if (output->CreateVideoFrame(outputFrameSize.width, outputFrameSize.height, outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK) 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."; error = "DeckLink output setup failed while creating an output video frame.";
return false; return false;
@@ -380,7 +386,7 @@ bool DeckLinkSession::ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hgl
outputVideoFrameQueue.push_back(outputFrame); outputVideoFrameQueue.push_back(outputFrame);
} }
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(owner)); playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
if (playoutDelegate == nullptr) if (playoutDelegate == nullptr)
{ {
error = "DeckLink output setup failed while creating the playout callback."; 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; return false;
} }
if (!formatStatusMessage.empty()) if (!mState.formatStatusMessage.empty())
statusMessage = statusMessage.empty() ? formatStatusMessage : formatStatusMessage + " " + statusMessage; mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;
return true; return true;
} }
double DeckLinkSession::FrameBudgetMilliseconds() const double DeckLinkSession::FrameBudgetMilliseconds() const
{ {
return frameTimescale != 0 return mScheduler.FrameBudgetMilliseconds();
? (static_cast<double>(frameDuration) * 1000.0) / static_cast<double>(frameTimescale)
: 0.0;
} }
IDeckLinkMutableVideoFrame* DeckLinkSession::RotateOutputFrame() bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
{ {
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front(); CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame); outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front(); outputVideoFrameQueue.pop_front();
return outputVideoFrame.p;
}
void DeckLinkSession::AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult) CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
{ if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
if (completionResult == bmdOutputFrameDisplayedLate || completionResult == bmdOutputFrameDropped) return false;
totalPlayoutFrames += 2;
} if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
return false;
bool DeckLinkSession::ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{ void* pFrame = nullptr;
if (output->ScheduleVideoFrame(outputVideoFrame, (totalPlayoutFrames * frameDuration), frameDuration, frameTimescale) != S_OK) 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<IDeckLinkVideoBuffer*>(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<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
if (outputVideoFrame == nullptr || output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
return false; return false;
totalPlayoutFrames++;
return true; return true;
} }
bool DeckLinkSession::Start() bool DeckLinkSession::Start()
{ {
totalPlayoutFrames = 0; mScheduler.Reset();
if (!output) if (!output)
{ {
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR); 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; return false;
} }
void* pFrame; void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes((void**)&pFrame); outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * outputFrameSize.height); memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); 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); MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false; return false;
} }
totalPlayoutFrames++;
} }
if (input) if (input)
@@ -485,7 +518,7 @@ bool DeckLinkSession::Start()
return false; 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); MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false; return false;
@@ -499,7 +532,7 @@ bool DeckLinkSession::Stop()
if (keyer != nullptr) if (keyer != nullptr)
{ {
keyer->Disable(); keyer->Disable();
externalKeyingActive = false; mState.externalKeyingActive = false;
} }
if (input) if (input)
@@ -516,3 +549,66 @@ bool DeckLinkSession::Stop()
return true; return true;
} }
void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
{
mState.hasInputSource = !hasNoInputSource;
if (hasNoInputSource || mInputFrameCallback == nullptr)
{
VideoIOFrame frame;
frame.width = mState.inputFrameSize.width;
frame.height = mState.inputFrameSize.height;
frame.pixelFormat = mState.inputPixelFormat;
frame.hasNoInputSource = hasNoInputSource;
if (mInputFrameCallback)
mInputFrameCallback(frame);
return;
}
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
void* videoPixels = nullptr;
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
return;
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
return;
inputFrameBuffer->GetBytes(&videoPixels);
VideoIOFrame frame;
frame.bytes = videoPixels;
frame.rowBytes = inputFrame->GetRowBytes();
frame.width = static_cast<unsigned>(inputFrame->GetWidth());
frame.height = static_cast<unsigned>(inputFrame->GetHeight());
frame.pixelFormat = mState.inputPixelFormat;
frame.hasNoInputSource = hasNoInputSource;
mInputFrameCallback(frame);
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
}
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame*, 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);
}

View File

@@ -3,7 +3,10 @@
#include "DeckLinkAPI_h.h" #include "DeckLinkAPI_h.h"
#include "DeckLinkDisplayMode.h" #include "DeckLinkDisplayMode.h"
#include "DeckLinkFrameTransfer.h" #include "DeckLinkFrameTransfer.h"
#include "DeckLinkVideoIOFormat.h"
#include "VideoIOFormat.h" #include "VideoIOFormat.h"
#include "VideoIOTypes.h"
#include "VideoPlayoutScheduler.h"
#include <atlbase.h> #include <atlbase.h>
#include <deque> #include <deque>
@@ -11,51 +14,56 @@
class OpenGLComposite; class OpenGLComposite;
class DeckLinkSession class DeckLinkSession : public VideoIODevice
{ {
public: public:
DeckLinkSession() = default; DeckLinkSession() = default;
~DeckLinkSession(); ~DeckLinkSession();
void ReleaseResources(); void ReleaseResources() override;
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error); bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error); bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) override;
bool ConfigureInput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& inputVideoMode, std::string& error); bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
bool ConfigureOutput(OpenGLComposite* owner, HDC hdc, HGLRC hglrc, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error); bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
bool Start(); bool Start() override;
bool Stop(); bool Stop() override;
bool HasInputDevice() const { return input != nullptr; } bool HasInputDevice() const { return mState.hasInputDevice; }
bool HasInputSource() const { return !hasNoInputSource; } bool HasInputSource() const { return mState.hasInputSource; }
void SetInputSourceMissing(bool missing) { hasNoInputSource = missing; } void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; }
bool InputOutputDimensionsDiffer() const { return inputFrameSize != outputFrameSize; } bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; }
const FrameSize& InputFrameSize() const { return inputFrameSize; } const FrameSize& InputFrameSize() const { return mState.inputFrameSize; }
const FrameSize& OutputFrameSize() const { return outputFrameSize; } const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; }
unsigned InputFrameWidth() const { return inputFrameSize.width; } unsigned InputFrameWidth() const { return mState.inputFrameSize.width; }
unsigned InputFrameHeight() const { return inputFrameSize.height; } unsigned InputFrameHeight() const { return mState.inputFrameSize.height; }
unsigned OutputFrameWidth() const { return outputFrameSize.width; } unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; }
unsigned OutputFrameHeight() const { return outputFrameSize.height; } unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; }
VideoIOPixelFormat InputPixelFormat() const { return inputPixelFormat; } VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; }
VideoIOPixelFormat OutputPixelFormat() const { return outputPixelFormat; } VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; }
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(inputPixelFormat); } bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); }
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(outputPixelFormat); } bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); }
unsigned InputFrameRowBytes() const { return inputFrameRowBytes; } unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; }
unsigned OutputFrameRowBytes() const { return outputFrameRowBytes; } unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; }
unsigned CaptureTextureWidth() const { return captureTextureWidth; } unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; }
unsigned OutputPackTextureWidth() const { return outputPackTextureWidth; } unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; }
const std::string& FormatStatusMessage() const { return formatStatusMessage; } const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; }
const std::string& InputDisplayModeName() const { return inputDisplayModeName; } const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; }
const std::string& OutputModelName() const { return outputModelName; } const std::string& OutputModelName() const { return mState.outputModelName; }
bool SupportsInternalKeying() const { return supportsInternalKeying; } bool SupportsInternalKeying() const { return mState.supportsInternalKeying; }
bool SupportsExternalKeying() const { return supportsExternalKeying; } bool SupportsExternalKeying() const { return mState.supportsExternalKeying; }
bool KeyerInterfaceAvailable() const { return keyerInterfaceAvailable; } bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; }
bool ExternalKeyingActive() const { return externalKeyingActive; } bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
const std::string& StatusMessage() const { return statusMessage; } const std::string& StatusMessage() const { return mState.statusMessage; }
void SetStatusMessage(const std::string& message) { statusMessage = message; } void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
const VideoIOState& State() const override { return mState; }
VideoIOState& MutableState() override { return mState; }
double FrameBudgetMilliseconds() const; double FrameBudgetMilliseconds() const;
IDeckLinkMutableVideoFrame* RotateOutputFrame(); void AccountForCompletionResult(VideoIOCompletionResult completionResult) override;
void AccountForCompletionResult(BMDOutputFrameCompletionResult completionResult); bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
bool ScheduleOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame); void EndOutputFrame(VideoIOOutputFrame& frame) override;
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
private: private:
CComPtr<CaptureDelegate> captureDelegate; CComPtr<CaptureDelegate> captureDelegate;
@@ -64,25 +72,8 @@ private:
CComPtr<IDeckLinkOutput> output; CComPtr<IDeckLinkOutput> output;
CComPtr<IDeckLinkKeyer> keyer; CComPtr<IDeckLinkKeyer> keyer;
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue; std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
BMDTimeValue frameDuration = 0; VideoIOState mState;
BMDTimeScale frameTimescale = 0; VideoPlayoutScheduler mScheduler;
unsigned totalPlayoutFrames = 0; InputFrameCallback mInputFrameCallback;
FrameSize inputFrameSize; OutputFrameCallback mOutputFrameCallback;
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;
}; };

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
#pragma once
#include "DeckLinkAPI_h.h"
#include "VideoIOFormat.h"
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);

View File

@@ -3,9 +3,9 @@
#include "OpenGLComposite.h" #include "OpenGLComposite.h"
#include "GLExtensions.h" #include "GLExtensions.h"
#include "GlRenderConstants.h" #include "GlRenderConstants.h"
#include "OpenGLDeckLinkBridge.h"
#include "OpenGLRenderPass.h" #include "OpenGLRenderPass.h"
#include "OpenGLShaderPrograms.h" #include "OpenGLShaderPrograms.h"
#include "OpenGLVideoIOBridge.h"
#include "PngScreenshotWriter.h" #include "PngScreenshotWriter.h"
#include "RuntimeServices.h" #include "RuntimeServices.h"
#include "ShaderBuildQueue.h" #include "ShaderBuildQueue.h"
@@ -22,15 +22,15 @@
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC), hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mDeckLink(std::make_unique<DeckLinkSession>()), mVideoIO(std::make_unique<DeckLinkSession>()),
mRenderer(std::make_unique<OpenGLRenderer>()), mRenderer(std::make_unique<OpenGLRenderer>()),
mUseCommittedLayerStates(false), mUseCommittedLayerStates(false),
mScreenshotRequested(false) mScreenshotRequested(false)
{ {
InitializeCriticalSection(&pMutex); InitializeCriticalSection(&pMutex);
mRuntimeHost = std::make_unique<RuntimeHost>(); mRuntimeHost = std::make_unique<RuntimeHost>();
mDeckLinkBridge = std::make_unique<OpenGLDeckLinkBridge>( mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
*mDeckLink, *mVideoIO,
*mRenderer, *mRenderer,
*mRuntimeHost, *mRuntimeHost,
pMutex, pMutex,
@@ -51,13 +51,18 @@ OpenGLComposite::~OpenGLComposite()
mRuntimeServices->Stop(); mRuntimeServices->Stop();
if (mShaderBuildQueue) if (mShaderBuildQueue)
mShaderBuildQueue->Stop(); mShaderBuildQueue->Stop();
mDeckLink->ReleaseResources(); mVideoIO->ReleaseResources();
mRenderer->DestroyResources(); mRenderer->DestroyResources();
DeleteCriticalSection(&pMutex); DeleteCriticalSection(&pMutex);
} }
bool OpenGLComposite::InitDeckLink() bool OpenGLComposite::InitDeckLink()
{
return InitVideoIO();
}
bool OpenGLComposite::InitVideoIO()
{ {
VideoFormatSelection videoModes; VideoFormatSelection videoModes;
std::string initFailureReason; 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." const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
? "This application requires the DeckLink drivers installed." ? "This application requires the DeckLink drivers installed."
@@ -95,7 +100,7 @@ bool OpenGLComposite::InitDeckLink()
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR); MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
return false; return false;
} }
if (!mDeckLink->SelectPreferredFormats(videoModes, initFailureReason)) if (!mVideoIO->SelectPreferredFormats(videoModes, initFailureReason))
goto error; goto error;
if (! CheckOpenGLExtensions()) if (! CheckOpenGLExtensions())
@@ -110,38 +115,38 @@ bool OpenGLComposite::InitDeckLink()
goto error; goto error;
} }
PublishDeckLinkOutputStatus(mDeckLink->OutputModelName().empty() PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
? "DeckLink output device selected." ? "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. // Resize window to match output video frame, but scale large formats down by half for viewing.
if (mDeckLink->OutputFrameWidth() < 1920) if (mVideoIO->OutputFrameWidth() < 1920)
resizeWindow(mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight()); resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
else 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; 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; goto error;
} }
PublishDeckLinkOutputStatus(mDeckLink->StatusMessage()); PublishVideoIOStatus(mVideoIO->StatusMessage());
return true; return true;
error: error:
if (!initFailureReason.empty()) if (!initFailureReason.empty())
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR); MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
mDeckLink->ReleaseResources(); mVideoIO->ReleaseResources();
return false; return false;
} }
@@ -153,7 +158,7 @@ void OpenGLComposite::paintGL()
return; return;
} }
mRenderer->PresentToWindow(hGLDC, mDeckLink->OutputFrameWidth(), mDeckLink->OutputFrameHeight()); mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
ValidateRect(hGLWnd, NULL); ValidateRect(hGLWnd, NULL);
LeaveCriticalSection(&pMutex); 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) if (!mRuntimeHost)
return; return;
if (!statusMessage.empty()) if (!statusMessage.empty())
mDeckLink->SetStatusMessage(statusMessage); mVideoIO->SetStatusMessage(statusMessage);
mRuntimeHost->SetDeckLinkOutputStatus( mRuntimeHost->SetVideoIOStatus(
mDeckLink->OutputModelName(), "decklink",
mDeckLink->SupportsInternalKeying(), mVideoIO->OutputModelName(),
mDeckLink->SupportsExternalKeying(), mVideoIO->SupportsInternalKeying(),
mDeckLink->KeyerInterfaceAvailable(), mVideoIO->SupportsExternalKeying(),
mVideoIO->KeyerInterfaceAvailable(),
mRuntimeHost->ExternalKeyingEnabled(), mRuntimeHost->ExternalKeyingEnabled(),
mDeckLink->ExternalKeyingActive(), mVideoIO->ExternalKeyingActive(),
mDeckLink->StatusMessage()); mVideoIO->StatusMessage());
} }
bool OpenGLComposite::InitOpenGLState() bool OpenGLComposite::InitOpenGLState()
@@ -223,7 +229,7 @@ bool OpenGLComposite::InitOpenGLState()
return false; 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); MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
return false; return false;
@@ -234,12 +240,12 @@ bool OpenGLComposite::InitOpenGLState()
std::string rendererError; std::string rendererError;
if (!mRenderer->InitializeResources( if (!mRenderer->InitializeResources(
mDeckLink->InputFrameWidth(), mVideoIO->InputFrameWidth(),
mDeckLink->InputFrameHeight(), mVideoIO->InputFrameHeight(),
mDeckLink->CaptureTextureWidth(), mVideoIO->CaptureTextureWidth(),
mDeckLink->OutputFrameWidth(), mVideoIO->OutputFrameWidth(),
mDeckLink->OutputFrameHeight(), mVideoIO->OutputFrameHeight(),
mDeckLink->OutputPackTextureWidth(), mVideoIO->OutputPackTextureWidth(),
rendererError)) rendererError))
{ {
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK); MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
@@ -251,20 +257,9 @@ bool OpenGLComposite::InitOpenGLState()
return true; 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() bool OpenGLComposite::Start()
{ {
return mDeckLink->Start(); return mVideoIO->Start();
} }
bool OpenGLComposite::Stop() bool OpenGLComposite::Stop()
@@ -272,10 +267,10 @@ bool OpenGLComposite::Stop()
if (mRuntimeServices) if (mRuntimeServices)
mRuntimeServices->Stop(); mRuntimeServices->Stop();
const bool wasExternalKeyingActive = mDeckLink->ExternalKeyingActive(); const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
mDeckLink->Stop(); mVideoIO->Stop();
if (wasExternalKeyingActive) if (wasExternalKeyingActive)
PublishDeckLinkOutputStatus("External keying has been disabled."); PublishVideoIOStatus("External keying has been disabled.");
return true; return true;
} }
@@ -303,7 +298,7 @@ void OpenGLComposite::renderEffect()
{ {
ProcessRuntimePollResults(); ProcessRuntimePollResults();
const bool hasInputSource = mDeckLink->HasInputSource(); const bool hasInputSource = mVideoIO->HasInputSource();
std::vector<RuntimeRenderState> layerStates; std::vector<RuntimeRenderState> layerStates;
if (mUseCommittedLayerStates) if (mUseCommittedLayerStates)
{ {
@@ -313,7 +308,7 @@ void OpenGLComposite::renderEffect()
} }
else if (mRuntimeHost) else if (mRuntimeHost)
{ {
if (mRuntimeHost->TryGetLayerRenderStates(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight(), layerStates)) if (mRuntimeHost->TryGetLayerRenderStates(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), layerStates))
{ {
mCachedLayerRenderStates = layerStates; mCachedLayerRenderStates = layerStates;
} }
@@ -327,10 +322,10 @@ void OpenGLComposite::renderEffect()
mRenderPass->Render( mRenderPass->Render(
hasInputSource, hasInputSource,
layerStates, layerStates,
mDeckLink->InputFrameWidth(), mVideoIO->InputFrameWidth(),
mDeckLink->InputFrameHeight(), mVideoIO->InputFrameHeight(),
mDeckLink->CaptureTextureWidth(), mVideoIO->CaptureTextureWidth(),
mDeckLink->InputPixelFormat(), mVideoIO->InputPixelFormat(),
historyCap, historyCap,
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) { [this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error); return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
@@ -345,8 +340,8 @@ void OpenGLComposite::ProcessScreenshotRequest()
if (!mScreenshotRequested.exchange(false)) if (!mScreenshotRequested.exchange(false))
return; return;
const unsigned width = mDeckLink ? mDeckLink->OutputFrameWidth() : 0; const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
const unsigned height = mDeckLink ? mDeckLink->OutputFrameHeight() : 0; const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
if (width == 0 || height == 0) if (width == 0 || height == 0)
return; return;
@@ -426,7 +421,7 @@ bool OpenGLComposite::ProcessRuntimePollResults()
return true; return true;
char compilerErrorMessage[1024] = {}; 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); mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
mUseCommittedLayerStates = true; mUseCommittedLayerStates = true;
@@ -449,13 +444,13 @@ bool OpenGLComposite::ProcessRuntimePollResults()
void OpenGLComposite::RequestShaderBuild() void OpenGLComposite::RequestShaderBuild()
{ {
if (!mShaderBuildQueue || !mDeckLink) if (!mShaderBuildQueue || !mVideoIO)
return; return;
mUseCommittedLayerStates = true; mUseCommittedLayerStates = true;
if (mRuntimeHost) if (mRuntimeHost)
mRuntimeHost->ClearReloadRequest(); mRuntimeHost->ClearReloadRequest();
mShaderBuildQueue->RequestBuild(mDeckLink->InputFrameWidth(), mDeckLink->InputFrameHeight()); mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
} }
void OpenGLComposite::broadcastRuntimeState() void OpenGLComposite::broadcastRuntimeState()

View File

@@ -10,7 +10,6 @@
#include <objbase.h> #include <objbase.h>
#include <atlbase.h> #include <atlbase.h>
#include <comutil.h> #include <comutil.h>
#include "DeckLinkAPI_h.h"
#include "GLExtensions.h" #include "GLExtensions.h"
#include "OpenGLRenderer.h" #include "OpenGLRenderer.h"
@@ -25,8 +24,8 @@
#include <vector> #include <vector>
#include <deque> #include <deque>
class DeckLinkSession; class VideoIODevice;
class OpenGLDeckLinkBridge; class OpenGLVideoIOBridge;
class OpenGLRenderPass; class OpenGLRenderPass;
class OpenGLShaderPrograms; class OpenGLShaderPrograms;
class RuntimeServices; class RuntimeServices;
@@ -40,6 +39,7 @@ public:
~OpenGLComposite(); ~OpenGLComposite();
bool InitDeckLink(); bool InitDeckLink();
bool InitVideoIO();
bool Start(); bool Start();
bool Stop(); bool Stop();
bool ReloadShader(); bool ReloadShader();
@@ -65,13 +65,10 @@ public:
void resizeGL(WORD width, WORD height); void resizeGL(WORD width, WORD height);
void paintGL(); void paintGL();
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
private: private:
void resizeWindow(int width, int height); void resizeWindow(int width, int height);
bool CheckOpenGLExtensions(); bool CheckOpenGLExtensions();
void PublishDeckLinkOutputStatus(const std::string& statusMessage); void PublishVideoIOStatus(const std::string& statusMessage);
using LayerProgram = OpenGLRenderer::LayerProgram; using LayerProgram = OpenGLRenderer::LayerProgram;
HWND hGLWnd; HWND hGLWnd;
@@ -79,10 +76,10 @@ private:
HGLRC hGLRC; HGLRC hGLRC;
CRITICAL_SECTION pMutex; CRITICAL_SECTION pMutex;
std::unique_ptr<DeckLinkSession> mDeckLink; std::unique_ptr<VideoIODevice> mVideoIO;
std::unique_ptr<OpenGLRenderer> mRenderer; std::unique_ptr<OpenGLRenderer> mRenderer;
std::unique_ptr<RuntimeHost> mRuntimeHost; std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<OpenGLDeckLinkBridge> mDeckLinkBridge; std::unique_ptr<OpenGLVideoIOBridge> mVideoIOBridge;
std::unique_ptr<OpenGLRenderPass> mRenderPass; std::unique_ptr<OpenGLRenderPass> mRenderPass;
std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms; std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms;
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue; std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;

View File

@@ -1,14 +1,13 @@
#include "OpenGLDeckLinkBridge.h" #include "OpenGLVideoIOBridge.h"
#include "DeckLinkSession.h"
#include "OpenGLRenderer.h" #include "OpenGLRenderer.h"
#include "RuntimeHost.h" #include "RuntimeHost.h"
#include <chrono> #include <chrono>
#include <gl/gl.h> #include <gl/gl.h>
OpenGLDeckLinkBridge::OpenGLDeckLinkBridge( OpenGLVideoIOBridge::OpenGLVideoIOBridge(
DeckLinkSession& deckLink, VideoIODevice& videoIO,
OpenGLRenderer& renderer, OpenGLRenderer& renderer,
RuntimeHost& runtimeHost, RuntimeHost& runtimeHost,
CRITICAL_SECTION& mutex, CRITICAL_SECTION& mutex,
@@ -17,7 +16,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
RenderEffectCallback renderEffect, RenderEffectCallback renderEffect,
OutputReadyCallback outputReady, OutputReadyCallback outputReady,
PaintCallback paint) : PaintCallback paint) :
mDeckLink(deckLink), mVideoIO(videoIO),
mRenderer(renderer), mRenderer(renderer),
mRuntimeHost(runtimeHost), mRuntimeHost(runtimeHost),
mMutex(mutex), 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(); const auto now = std::chrono::steady_clock::now();
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point()) if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
@@ -44,11 +43,11 @@ void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult comp
} }
mLastPlayoutCompletionTime = now; mLastPlayoutCompletionTime = now;
if (completionResult == bmdOutputFrameDisplayedLate) if (completionResult == VideoIOCompletionResult::DisplayedLate)
++mLateFrameCount; ++mLateFrameCount;
else if (completionResult == bmdOutputFrameDropped) else if (completionResult == VideoIOCompletionResult::Dropped)
++mDroppedFrameCount; ++mDroppedFrameCount;
else if (completionResult == bmdOutputFrameFlushed) else if (completionResult == VideoIOCompletionResult::Flushed)
++mFlushedFrameCount; ++mFlushedFrameCount;
mRuntimeHost.TrySetFramePacingStats( mRuntimeHost.TrySetFramePacingStats(
@@ -60,27 +59,15 @@ void OpenGLDeckLinkBridge::RecordFramePacing(BMDOutputFrameCompletionResult comp
mFlushedFrameCount); mFlushedFrameCount);
} }
void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource) void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
{ {
mDeckLink.SetInputSourceMissing(hasNoInputSource); const VideoIOState& state = mVideoIO.State();
mRuntimeHost.TrySetSignalStatus(!hasNoInputSource, mDeckLink.InputFrameWidth(), mDeckLink.InputFrameHeight(), mDeckLink.InputDisplayModeName()); 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 return; // don't transfer texture when there's no input
long textureSize = inputFrame->GetRowBytes() * inputFrame->GetHeight(); const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
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);
EnterCriticalSection(&mMutex); EnterCriticalSection(&mMutex);
@@ -89,14 +76,14 @@ void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFram
glPixelStorei(GL_UNPACK_ALIGNMENT, 4); glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer()); 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()); glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data. // NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data.
if (mDeckLink.InputPixelFormat() == VideoIOPixelFormat::V210) if (inputFrame.pixelFormat == VideoIOPixelFormat::V210)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mDeckLink.CaptureTextureWidth(), mDeckLink.InputFrameHeight(), GL_RGBA, GL_UNSIGNED_BYTE, NULL); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
else 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); glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
@@ -104,21 +91,24 @@ void OpenGLDeckLinkBridge::VideoFrameArrived(IDeckLinkVideoInputFrame* inputFram
wglMakeCurrent(NULL, NULL); wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex); LeaveCriticalSection(&mMutex);
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
inputFrameBuffer->Release();
} }
void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult) void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
{ {
(void)completedFrame; RecordFramePacing(completion.result);
RecordFramePacing(completionResult);
EnterCriticalSection(&mMutex); EnterCriticalSection(&mMutex);
// Get the first frame from the queue VideoIOOutputFrame outputFrame;
IDeckLinkMutableVideoFrame* outputVideoFrame = mDeckLink.RotateOutputFrame(); 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 // make GL context current in this thread
wglMakeCurrent(mHdc, mHglrc); wglMakeCurrent(mHdc, mHglrc);
@@ -129,14 +119,14 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF
mRenderEffect(); mRenderEffect();
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer()); glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer()); 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()); glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
if (mOutputReady) if (mOutputReady)
mOutputReady(); mOutputReady();
if (mDeckLink.OutputIsTenBit()) if (state.outputPixelFormat == VideoIOPixelFormat::V210)
{ {
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer()); 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_SCISSOR_TEST);
glDisable(GL_BLEND); glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST); glDisable(GL_DEPTH_TEST);
@@ -147,9 +137,9 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution"); const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words"); const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
if (outputResolutionLocation >= 0) if (outputResolutionLocation >= 0)
glUniform2f(outputResolutionLocation, static_cast<float>(mDeckLink.OutputFrameWidth()), static_cast<float>(mDeckLink.OutputFrameHeight())); glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
if (activeWordsLocation >= 0) if (activeWordsLocation >= 0)
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(mDeckLink.OutputFrameWidth()))); glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
glDrawArrays(GL_TRIANGLES, 0, 3); glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0); glUseProgram(0);
glBindVertexArray(0); glBindVertexArray(0);
@@ -157,51 +147,31 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF
} }
glFlush(); glFlush();
const auto renderEndTime = std::chrono::steady_clock::now(); 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<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count(); const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds); mRuntimeHost.TrySetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
mRuntimeHost.TryAdvanceFrame(); 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_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0); glPixelStorei(GL_PACK_ROW_LENGTH, 0);
if (mDeckLink.OutputIsTenBit()) if (state.outputPixelFormat == VideoIOPixelFormat::V210)
{ {
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer()); 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 else
{ {
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer()); 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(); mPaint();
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite); mVideoIO.EndOutputFrame(outputFrame);
outputVideoFrameBuffer->Release();
mDeckLink.AccountForCompletionResult(completionResult); mVideoIO.AccountForCompletionResult(completion.result);
// Schedule the next frame for playout // Schedule the next frame for playout
mDeckLink.ScheduleOutputFrame(outputVideoFrame); mVideoIO.ScheduleOutputFrame(outputFrame);
wglMakeCurrent(NULL, NULL); wglMakeCurrent(NULL, NULL);

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include "DeckLinkAPI_h.h" #include "VideoIOTypes.h"
#include <windows.h> #include <windows.h>
@@ -8,19 +8,24 @@
#include <functional> #include <functional>
#include <cstdint> #include <cstdint>
class DeckLinkSession;
class OpenGLRenderer; class OpenGLRenderer;
class RuntimeHost; class RuntimeHost;
class OpenGLDeckLinkBridge struct RenderPipelineFrameContext
{
VideoIOState videoState;
VideoIOCompletion completion;
};
class OpenGLVideoIOBridge
{ {
public: public:
using RenderEffectCallback = std::function<void()>; using RenderEffectCallback = std::function<void()>;
using OutputReadyCallback = std::function<void()>; using OutputReadyCallback = std::function<void()>;
using PaintCallback = std::function<void()>; using PaintCallback = std::function<void()>;
OpenGLDeckLinkBridge( OpenGLVideoIOBridge(
DeckLinkSession& deckLink, VideoIODevice& videoIO,
OpenGLRenderer& renderer, OpenGLRenderer& renderer,
RuntimeHost& runtimeHost, RuntimeHost& runtimeHost,
CRITICAL_SECTION& mutex, CRITICAL_SECTION& mutex,
@@ -30,13 +35,13 @@ public:
OutputReadyCallback outputReady, OutputReadyCallback outputReady,
PaintCallback paint); PaintCallback paint);
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource); void VideoFrameArrived(const VideoIOFrame& inputFrame);
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult); void PlayoutFrameCompleted(const VideoIOCompletion& completion);
private: private:
void RecordFramePacing(BMDOutputFrameCompletionResult completionResult); void RecordFramePacing(VideoIOCompletionResult completionResult);
DeckLinkSession& mDeckLink; VideoIODevice& mVideoIO;
OpenGLRenderer& mRenderer; OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost; RuntimeHost& mRuntimeHost;
CRITICAL_SECTION& mMutex; CRITICAL_SECTION& mMutex;

View File

@@ -1213,8 +1213,16 @@ void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) 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<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
mDeckLinkOutputStatus.backendName = backendName;
mDeckLinkOutputStatus.modelName = modelName; mDeckLinkOutputStatus.modelName = modelName;
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying; mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying;
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying; mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying;
@@ -1889,6 +1897,17 @@ JsonValue RuntimeHost::BuildStateValue() const
deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage)); deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
root.set("decklink", deckLink); 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(); JsonValue performance = JsonValue::MakeObject();
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds)); performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
performance.set("renderMs", JsonValue(mRenderMilliseconds)); performance.set("renderMs", JsonValue(mRenderMilliseconds));

View File

@@ -39,6 +39,8 @@ public:
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName); bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage); 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); void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds); bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
@@ -86,6 +88,7 @@ private:
struct DeckLinkOutputStatus struct DeckLinkOutputStatus
{ {
std::string backendName = "decklink";
std::string modelName; std::string modelName;
bool supportsInternalKeying = false; bool supportsInternalKeying = false;
bool supportsExternalKeying = false; bool supportsExternalKeying = false;

View File

@@ -50,7 +50,16 @@ uint16_t Component(uint32_t word, unsigned index)
const char* VideoIOPixelFormatName(VideoIOPixelFormat format) 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) bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
@@ -58,21 +67,32 @@ bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
return format == VideoIOPixelFormat::V210; 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) VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
{ {
return tenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Uyvy8; 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) unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes)
{ {
return (rowBytes + 3u) / 4u; return (rowBytes + 3u) / 4u;

View File

@@ -1,14 +1,13 @@
#pragma once #pragma once
#include "DeckLinkAPI_h.h"
#include <array> #include <array>
#include <cstdint> #include <cstdint>
enum class VideoIOPixelFormat enum class VideoIOPixelFormat
{ {
Uyvy8, Uyvy8,
V210 V210,
Bgra8
}; };
struct V210CodeValues struct V210CodeValues
@@ -27,9 +26,9 @@ struct V210SixPixelBlock
const char* VideoIOPixelFormatName(VideoIOPixelFormat format); const char* VideoIOPixelFormatName(VideoIOPixelFormat format);
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format); bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format);
BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format);
VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format);
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported); VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported);
unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format);
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth);
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes); unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes);
unsigned MinimumV210RowBytes(unsigned frameWidth); unsigned MinimumV210RowBytes(unsigned frameWidth);
unsigned ActiveV210WordsForWidth(unsigned frameWidth); unsigned ActiveV210WordsForWidth(unsigned frameWidth);

View File

@@ -0,0 +1,137 @@
#pragma once
#include "DeckLinkDisplayMode.h"
#include "VideoIOFormat.h"
#include <cstdint>
#include <functional>
#include <string>
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<void(const VideoIOFrame&)>;
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
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; }
};

View File

@@ -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<int64_t>(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<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
: 0.0;
}

View File

@@ -0,0 +1,22 @@
#pragma once
#include "VideoIOTypes.h"
#include <cstdint>
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;
};

View File

@@ -0,0 +1,151 @@
#include "VideoIOTypes.h"
#include <array>
#include <iostream>
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<long>(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<long>(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<unsigned char, 3840> mInputBytes = {};
std::array<unsigned char, 7680> 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;
}

View File

@@ -1,4 +1,5 @@
#include "VideoIOFormat.h" #include "VideoIOFormat.h"
#include "DeckLinkVideoIOFormat.h"
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
@@ -22,6 +23,7 @@ void TestPreferredFormatSelection()
Expect(ChoosePreferredVideoIOFormat(false) == VideoIOPixelFormat::Uyvy8, "8-bit is used as fallback"); 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::V210) == bmdFormat10BitYUV, "v210 maps to DeckLink 10-bit YUV");
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Uyvy8) == bmdFormat8BitYUV, "UYVY maps to DeckLink 8-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() void TestRowByteHelpers()
@@ -31,6 +33,9 @@ void TestRowByteHelpers()
Expect(MinimumV210RowBytes(3840) == 10240, "3840-wide v210 active row bytes"); 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(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(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() void TestV210PackUnpack()

View File

@@ -0,0 +1,81 @@
#include "VideoPlayoutScheduler.h"
#include <cmath>
#include <iostream>
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;
}