PNG writer
This commit is contained in:
@@ -99,7 +99,7 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<TargetMachine>MachineX86</TargetMachine>
|
||||
@@ -121,7 +121,7 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<TargetMachine>MachineX64</TargetMachine>
|
||||
@@ -141,7 +141,7 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
@@ -166,7 +166,7 @@
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
@@ -181,6 +181,7 @@
|
||||
<ClCompile Include="gl\OpenGLRenderPass.cpp" />
|
||||
<ClCompile Include="gl\OpenGLRenderer.cpp" />
|
||||
<ClCompile Include="gl\OpenGLShaderPrograms.cpp" />
|
||||
<ClCompile Include="gl\PngScreenshotWriter.cpp" />
|
||||
<ClCompile Include="gl\ShaderBuildQueue.cpp" />
|
||||
<ClCompile Include="gl\TemporalHistoryBuffers.cpp" />
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
@@ -202,6 +203,7 @@
|
||||
<ClInclude Include="gl\OpenGLRenderPass.h" />
|
||||
<ClInclude Include="gl\OpenGLRenderer.h" />
|
||||
<ClInclude Include="gl\OpenGLShaderPrograms.h" />
|
||||
<ClInclude Include="gl\PngScreenshotWriter.h" />
|
||||
<ClInclude Include="gl\ShaderBuildQueue.h" />
|
||||
<ClInclude Include="gl\TemporalHistoryBuffers.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<ClCompile Include="gl\OpenGLShaderPrograms.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\PngScreenshotWriter.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\ShaderBuildQueue.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -80,6 +83,9 @@
|
||||
<ClInclude Include="gl\OpenGLShaderPrograms.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\PngScreenshotWriter.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\ShaderBuildQueue.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -433,6 +433,11 @@ bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& ro
|
||||
{
|
||||
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
||||
}
|
||||
},
|
||||
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
|
||||
{
|
||||
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
||||
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
||||
std::function<bool(std::string&)> reloadShader;
|
||||
std::function<bool(std::string&)> requestScreenshot;
|
||||
};
|
||||
|
||||
ControlServer();
|
||||
|
||||
@@ -26,6 +26,7 @@ bool StartRuntimeControlServices(
|
||||
callbacks.resetLayerParameters = [&composite](const std::string& layerId, std::string& actionError) { return composite.ResetLayerParameters(layerId, actionError); };
|
||||
callbacks.saveStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.SaveStackPreset(presetName, actionError); };
|
||||
callbacks.loadStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.LoadStackPreset(presetName, actionError); };
|
||||
callbacks.requestScreenshot = [&composite](std::string& actionError) { return composite.RequestScreenshot(actionError); };
|
||||
callbacks.reloadShader = [&composite](std::string& actionError) {
|
||||
if (!composite.ReloadShader())
|
||||
{
|
||||
|
||||
@@ -6,10 +6,17 @@
|
||||
#include "OpenGLDeckLinkBridge.h"
|
||||
#include "OpenGLRenderPass.h"
|
||||
#include "OpenGLShaderPrograms.h"
|
||||
#include "PngScreenshotWriter.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -17,7 +24,8 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
||||
mDeckLink(std::make_unique<DeckLinkSession>()),
|
||||
mRenderer(std::make_unique<OpenGLRenderer>()),
|
||||
mUseCommittedLayerStates(false)
|
||||
mUseCommittedLayerStates(false),
|
||||
mScreenshotRequested(false)
|
||||
{
|
||||
InitializeCriticalSection(&pMutex);
|
||||
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||
@@ -29,6 +37,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||
hGLDC,
|
||||
hGLRC,
|
||||
[this]() { renderEffect(); },
|
||||
[this]() { ProcessScreenshotRequest(); },
|
||||
[this]() { paintGL(); });
|
||||
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
|
||||
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
|
||||
@@ -283,6 +292,13 @@ bool OpenGLComposite::ReloadShader()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
||||
{
|
||||
(void)error;
|
||||
mScreenshotRequested.store(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLComposite::renderEffect()
|
||||
{
|
||||
ProcessRuntimePollResults();
|
||||
@@ -324,6 +340,69 @@ void OpenGLComposite::renderEffect()
|
||||
});
|
||||
}
|
||||
|
||||
void OpenGLComposite::ProcessScreenshotRequest()
|
||||
{
|
||||
if (!mScreenshotRequested.exchange(false))
|
||||
return;
|
||||
|
||||
const unsigned width = mDeckLink ? mDeckLink->OutputFrameWidth() : 0;
|
||||
const unsigned height = mDeckLink ? mDeckLink->OutputFrameHeight() : 0;
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
|
||||
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
|
||||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
{
|
||||
const unsigned sourceY = height - 1 - y;
|
||||
std::copy(
|
||||
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
|
||||
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
|
||||
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const std::filesystem::path outputPath = BuildScreenshotPath();
|
||||
std::filesystem::create_directories(outputPath.parent_path());
|
||||
WritePngFileAsync(outputPath, width, height, std::move(topDownPixels));
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
|
||||
{
|
||||
const std::filesystem::path root = mRuntimeHost && !mRuntimeHost->GetRuntimeRoot().empty()
|
||||
? mRuntimeHost->GetRuntimeRoot()
|
||||
: std::filesystem::current_path();
|
||||
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
||||
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm localTime = {};
|
||||
localtime_s(&localTime, &nowTime);
|
||||
|
||||
std::ostringstream filename;
|
||||
filename << "video-shader-toys-"
|
||||
<< std::put_time(&localTime, "%Y%m%d-%H%M%S")
|
||||
<< "-" << std::setw(3) << std::setfill('0') << milliseconds.count()
|
||||
<< ".png";
|
||||
|
||||
return root / "screenshots" / filename.str();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ProcessRuntimePollResults()
|
||||
{
|
||||
if (!mRuntimeHost || !mRuntimeServices)
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -54,6 +55,7 @@ public:
|
||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||
bool RequestScreenshot(std::string& error);
|
||||
unsigned short GetControlServerPort() const;
|
||||
unsigned short GetOscPort() const;
|
||||
std::string GetControlUrl() const;
|
||||
@@ -87,11 +89,14 @@ private:
|
||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||
std::atomic<bool> mUseCommittedLayerStates;
|
||||
std::atomic<bool> mScreenshotRequested;
|
||||
|
||||
bool InitOpenGLState();
|
||||
void renderEffect();
|
||||
bool ProcessRuntimePollResults();
|
||||
void RequestShaderBuild();
|
||||
void ProcessScreenshotRequest();
|
||||
std::filesystem::path BuildScreenshotPath() const;
|
||||
void broadcastRuntimeState();
|
||||
void resetTemporalHistoryState();
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint) :
|
||||
mDeckLink(deckLink),
|
||||
mRenderer(renderer),
|
||||
@@ -23,6 +24,7 @@ OpenGLDeckLinkBridge::OpenGLDeckLinkBridge(
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc),
|
||||
mRenderEffect(renderEffect),
|
||||
mOutputReady(outputReady),
|
||||
mPaint(paint)
|
||||
{
|
||||
}
|
||||
@@ -129,6 +131,8 @@ void OpenGLDeckLinkBridge::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedF
|
||||
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);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
if (mOutputReady)
|
||||
mOutputReady();
|
||||
if (mDeckLink.OutputIsTenBit())
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
|
||||
@@ -16,6 +16,7 @@ class OpenGLDeckLinkBridge
|
||||
{
|
||||
public:
|
||||
using RenderEffectCallback = std::function<void()>;
|
||||
using OutputReadyCallback = std::function<void()>;
|
||||
using PaintCallback = std::function<void()>;
|
||||
|
||||
OpenGLDeckLinkBridge(
|
||||
@@ -26,6 +27,7 @@ public:
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint);
|
||||
|
||||
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
|
||||
@@ -41,6 +43,7 @@ private:
|
||||
HDC mHdc;
|
||||
HGLRC mHglrc;
|
||||
RenderEffectCallback mRenderEffect;
|
||||
OutputReadyCallback mOutputReady;
|
||||
PaintCallback mPaint;
|
||||
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
||||
double mCompletionIntervalMilliseconds = 0.0;
|
||||
|
||||
133
apps/LoopThroughWithOpenGLCompositing/gl/PngScreenshotWriter.cpp
Normal file
133
apps/LoopThroughWithOpenGLCompositing/gl/PngScreenshotWriter.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "PngScreenshotWriter.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <wincodec.h>
|
||||
#include <atlbase.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string HResultToString(HRESULT hr)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << "HRESULT 0x" << std::hex << static_cast<unsigned long>(hr);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool WritePngFile(
|
||||
const std::filesystem::path& outputPath,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
const std::vector<unsigned char>& rgbaPixels,
|
||||
std::string& error)
|
||||
{
|
||||
if (width == 0 || height == 0 || rgbaPixels.size() < static_cast<std::size_t>(width) * height * 4)
|
||||
{
|
||||
error = "Invalid screenshot dimensions or pixel buffer.";
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT initializeResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
const bool shouldUninitialize = SUCCEEDED(initializeResult);
|
||||
if (FAILED(initializeResult) && initializeResult != RPC_E_CHANGED_MODE)
|
||||
{
|
||||
error = "CoInitializeEx failed: " + HResultToString(initializeResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICImagingFactory> factory;
|
||||
HRESULT result = CoCreateInstance(
|
||||
CLSID_WICImagingFactory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&factory));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC imaging factory: " + HResultToString(result);
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICStream> stream;
|
||||
result = factory->CreateStream(&stream);
|
||||
if (SUCCEEDED(result))
|
||||
result = stream->InitializeFromFilename(outputPath.wstring().c_str(), GENERIC_WRITE);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not open screenshot output file: " + HResultToString(result);
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICBitmapEncoder> encoder;
|
||||
result = factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder);
|
||||
if (SUCCEEDED(result))
|
||||
result = encoder->Initialize(stream, WICBitmapEncoderNoCache);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not initialize PNG encoder: " + HResultToString(result);
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICBitmapFrameEncode> frame;
|
||||
CComPtr<IPropertyBag2> propertyBag;
|
||||
result = encoder->CreateNewFrame(&frame, &propertyBag);
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->Initialize(propertyBag);
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->SetSize(width, height);
|
||||
|
||||
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppRGBA;
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->SetPixelFormat(&pixelFormat);
|
||||
if (SUCCEEDED(result) && pixelFormat != GUID_WICPixelFormat32bppRGBA)
|
||||
{
|
||||
error = "PNG encoder did not accept RGBA pixel format.";
|
||||
result = E_FAIL;
|
||||
}
|
||||
|
||||
const UINT stride = width * 4;
|
||||
const UINT imageSize = stride * height;
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->WritePixels(height, stride, imageSize, const_cast<BYTE*>(rgbaPixels.data()));
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->Commit();
|
||||
if (SUCCEEDED(result))
|
||||
result = encoder->Commit();
|
||||
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not write screenshot PNG: " + HResultToString(result);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void WritePngFileAsync(
|
||||
const std::filesystem::path& outputPath,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
std::vector<unsigned char> rgbaPixels)
|
||||
{
|
||||
std::thread(
|
||||
[outputPath, width, height, pixels = std::move(rgbaPixels)]() mutable
|
||||
{
|
||||
std::string error;
|
||||
if (!WritePngFile(outputPath, width, height, pixels, error))
|
||||
OutputDebugStringA(("Screenshot write failed: " + error + "\n").c_str());
|
||||
else
|
||||
OutputDebugStringA(("Screenshot written: " + outputPath.string() + "\n").c_str());
|
||||
}).detach();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
void WritePngFileAsync(
|
||||
const std::filesystem::path& outputPath,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
std::vector<unsigned char> rgbaPixels);
|
||||
|
||||
Reference in New Issue
Block a user