Enable GIF support for ZoomIt (#43266)

Closes #43265
This commit is contained in:
Mario Hewardt
2025-11-10 11:57:13 -08:00
committed by GitHub
parent a33c484c93
commit a271a2f8af
14 changed files with 1411 additions and 468 deletions

View File

@@ -66,6 +66,7 @@ APIIs
Apm
APPBARDATA
APPEXECLINK
appext
APPLICATIONFRAMEHOST
appmanifest
APPMODEL
@@ -187,6 +188,7 @@ CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
CAtl
CBN
cch
CCHDEVICENAME
CCHFORMNAME
@@ -414,6 +416,9 @@ DNLEN
DONOTROUND
DONTVALIDATEPATH
dotnet
downsampled
downsampling
Downsampled
downscale
DPICHANGED
DPIs
@@ -598,6 +603,7 @@ getfilesiginforedist
geolocator
GETHOTKEY
GETICON
GETLBTEXT
GETMINMAXINFO
GETNONCLIENTMETRICS
GETPROPERTYSTOREFLAGS
@@ -605,6 +611,7 @@ GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
GIFs
gitmodules
GHND
GMEM
@@ -615,6 +622,7 @@ GPOCA
gpp
gpu
gradians
grctlext
Gridcustomlayout
GSM
gtm

View File

@@ -0,0 +1,548 @@
//==============================================================================
//
// Zoomit
// Sysinternals - www.sysinternals.com
//
// GIF recording support using Windows Imaging Component (WIC)
//
//==============================================================================
#include "pch.h"
#include "GifRecordingSession.h"
#include "CaptureFrameWait.h"
#include <shcore.h>
extern DWORD g_RecordScaling;
namespace winrt
{
using namespace Windows::Foundation;
using namespace Windows::Graphics;
using namespace Windows::Graphics::Capture;
using namespace Windows::Graphics::DirectX;
using namespace Windows::Graphics::DirectX::Direct3D11;
using namespace Windows::Storage;
using namespace Windows::UI::Composition;
}
namespace util
{
using namespace robmikh::common::uwp;
}
const float CLEAR_COLOR[] = { 0.0f, 0.0f, 0.0f, 1.0f };
int32_t EnsureEvenGif(int32_t value)
{
if (value % 2 == 0)
{
return value;
}
else
{
return value + 1;
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::GifRecordingSession
//
//----------------------------------------------------------------------------
GifRecordingSession::GifRecordingSession(
winrt::IDirect3DDevice const& device,
winrt::GraphicsCaptureItem const& item,
RECT const cropRect,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream)
{
m_device = device;
m_d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(m_device);
m_d3dDevice->GetImmediateContext(m_d3dContext.put());
m_item = item;
m_frameRate = frameRate;
m_stream = stream;
auto itemSize = item.Size();
auto inputWidth = EnsureEvenGif(itemSize.Width);
auto inputHeight = EnsureEvenGif(itemSize.Height);
m_frameWait = std::make_shared<CaptureFrameWait>(m_device, m_item, winrt::SizeInt32{ inputWidth, inputHeight });
auto weakPointer{ std::weak_ptr{ m_frameWait } };
m_itemClosed = item.Closed(winrt::auto_revoke, [weakPointer](auto&, auto&)
{
auto sharedPointer{ weakPointer.lock() };
if (sharedPointer)
{
sharedPointer->StopCapture();
}
});
// Get crop dimension
if ((cropRect.right - cropRect.left) != 0)
{
m_rcCrop = cropRect;
m_frameWait->ShowCaptureBorder(false);
}
else
{
m_rcCrop.left = 0;
m_rcCrop.top = 0;
m_rcCrop.right = inputWidth;
m_rcCrop.bottom = inputHeight;
}
// Apply scaling
constexpr int c_minimumSize = 34;
auto scaledWidth = MulDiv(m_rcCrop.right - m_rcCrop.left, g_RecordScaling, 100);
auto scaledHeight = MulDiv(m_rcCrop.bottom - m_rcCrop.top, g_RecordScaling, 100);
m_width = scaledWidth;
m_height = scaledHeight;
if (m_width < c_minimumSize)
{
m_width = c_minimumSize;
m_height = MulDiv(m_height, m_width, scaledWidth);
}
if (m_height < c_minimumSize)
{
m_height = c_minimumSize;
m_width = MulDiv(m_width, m_height, scaledHeight);
}
if (m_width > inputWidth)
{
m_width = inputWidth;
m_height = c_minimumSize, MulDiv(m_height, scaledWidth, m_width);
}
if (m_height > inputHeight)
{
m_height = inputHeight;
m_width = c_minimumSize, MulDiv(m_width, scaledHeight, m_height);
}
m_width = EnsureEvenGif(m_width);
m_height = EnsureEvenGif(m_height);
m_frameDelay = (frameRate > 0) ? (100 / frameRate) : 15;
// Initialize WIC
winrt::check_hresult(CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(m_wicFactory.put())));
// Create WIC stream from IRandomAccessStream
winrt::check_hresult(m_wicFactory->CreateStream(m_wicStream.put()));
// Get the IStream from the IRandomAccessStream
winrt::com_ptr<IStream> streamInterop;
winrt::check_hresult(CreateStreamOverRandomAccessStream(
winrt::get_unknown(stream),
IID_PPV_ARGS(streamInterop.put())));
winrt::check_hresult(m_wicStream->InitializeFromIStream(streamInterop.get()));
// Create GIF encoder
winrt::check_hresult(m_wicFactory->CreateEncoder(
GUID_ContainerFormatGif,
nullptr,
m_gifEncoder.put()));
winrt::check_hresult(m_gifEncoder->Initialize(m_wicStream.get(), WICBitmapEncoderNoCache));
// Set global GIF metadata for looping (NETSCAPE2.0 application extension)
try
{
winrt::com_ptr<IWICMetadataQueryWriter> encoderMetadataWriter;
if (SUCCEEDED(m_gifEncoder->GetMetadataQueryWriter(encoderMetadataWriter.put())) && encoderMetadataWriter)
{
OutputDebugStringW(L"Setting NETSCAPE2.0 looping extension on encoder...\n");
// Set application extension
PROPVARIANT propValue;
PropVariantInit(&propValue);
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 11;
propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(11));
if (propValue.caub.pElems != nullptr)
{
memcpy(propValue.caub.pElems, "NETSCAPE2.0", 11);
HRESULT hr = encoderMetadataWriter->SetMetadataByName(L"/appext/application", &propValue);
if (SUCCEEDED(hr))
{
OutputDebugStringW(L"Encoder application extension set successfully\n");
}
else
{
OutputDebugStringW(L"Failed to set encoder application extension\n");
}
PropVariantClear(&propValue);
// Set loop count (0 = infinite)
PropVariantInit(&propValue);
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 5;
propValue.caub.pElems = static_cast<UCHAR*>(CoTaskMemAlloc(5));
if (propValue.caub.pElems != nullptr)
{
propValue.caub.pElems[0] = 3;
propValue.caub.pElems[1] = 1;
propValue.caub.pElems[2] = 0;
propValue.caub.pElems[3] = 0;
propValue.caub.pElems[4] = 0;
hr = encoderMetadataWriter->SetMetadataByName(L"/appext/data", &propValue);
if (SUCCEEDED(hr))
{
OutputDebugStringW(L"Encoder loop count set successfully\n");
}
else
{
OutputDebugStringW(L"Failed to set encoder loop count\n");
}
PropVariantClear(&propValue);
}
}
}
else
{
OutputDebugStringW(L"Failed to get encoder metadata writer\n");
}
}
catch (...)
{
OutputDebugStringW(L"Warning: Failed to set GIF encoder looping metadata\n");
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::~GifRecordingSession
//
//----------------------------------------------------------------------------
GifRecordingSession::~GifRecordingSession()
{
Close();
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::Create
//
//----------------------------------------------------------------------------
std::shared_ptr<GifRecordingSession> GifRecordingSession::Create(
winrt::IDirect3DDevice const& device,
winrt::GraphicsCaptureItem const& item,
RECT const& crop,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream)
{
return std::shared_ptr<GifRecordingSession>(new GifRecordingSession(device, item, crop, frameRate, stream));
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::EncodeFrame
//
//----------------------------------------------------------------------------
HRESULT GifRecordingSession::EncodeFrame(ID3D11Texture2D* frameTexture)
{
try
{
// Create a staging texture for CPU access
D3D11_TEXTURE2D_DESC frameDesc;
frameTexture->GetDesc(&frameDesc);
// GIF encoding with palette generation is VERY slow at high resolutions (4K takes 1 second per frame!)
UINT targetWidth = frameDesc.Width;
UINT targetHeight = frameDesc.Height;
if (frameDesc.Width > static_cast<uint32_t>(m_width) || frameDesc.Height > static_cast<uint32_t>(m_height))
{
float scaleX = static_cast<float>(m_width) / frameDesc.Width;
float scaleY = static_cast<float>(m_height) / frameDesc.Height;
float scale = min(scaleX, scaleY);
targetWidth = static_cast<UINT>(frameDesc.Width * scale);
targetHeight = static_cast<UINT>(frameDesc.Height * scale);
// Ensure even dimensions for GIF
targetWidth = (targetWidth / 2) * 2;
targetHeight = (targetHeight / 2) * 2;
}
D3D11_TEXTURE2D_DESC stagingDesc = frameDesc;
stagingDesc.Usage = D3D11_USAGE_STAGING;
stagingDesc.BindFlags = 0;
stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
stagingDesc.MiscFlags = 0;
winrt::com_ptr<ID3D11Texture2D> stagingTexture;
winrt::check_hresult(m_d3dDevice->CreateTexture2D(&stagingDesc, nullptr, stagingTexture.put()));
// Copy the frame to staging texture
m_d3dContext->CopyResource(stagingTexture.get(), frameTexture);
// Map the staging texture
D3D11_MAPPED_SUBRESOURCE mappedResource;
winrt::check_hresult(m_d3dContext->Map(stagingTexture.get(), 0, D3D11_MAP_READ, 0, &mappedResource));
// Create a new frame in the GIF
winrt::com_ptr<IWICBitmapFrameEncode> frameEncode;
winrt::com_ptr<IPropertyBag2> propertyBag;
winrt::check_hresult(m_gifEncoder->CreateNewFrame(frameEncode.put(), propertyBag.put()));
// Initialize the frame encoder with property bag
winrt::check_hresult(frameEncode->Initialize(propertyBag.get()));
// CRITICAL: For GIF, we MUST set size and pixel format BEFORE WriteSource
// Use target dimensions (may be downsampled)
winrt::check_hresult(frameEncode->SetSize(targetWidth, targetHeight));
// Set the pixel format to 8-bit indexed (required for GIF)
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat8bppIndexed;
winrt::check_hresult(frameEncode->SetPixelFormat(&pixelFormat));
// Create a WIC bitmap from the BGRA texture data
winrt::com_ptr<IWICBitmap> sourceBitmap;
winrt::check_hresult(m_wicFactory->CreateBitmapFromMemory(
frameDesc.Width,
frameDesc.Height,
GUID_WICPixelFormat32bppBGRA,
mappedResource.RowPitch,
frameDesc.Height * mappedResource.RowPitch,
static_cast<BYTE*>(mappedResource.pData),
sourceBitmap.put()));
// If we need downsampling, use WIC scaler
winrt::com_ptr<IWICBitmapSource> finalSource = sourceBitmap;
if (targetWidth != frameDesc.Width || targetHeight != frameDesc.Height)
{
winrt::com_ptr<IWICBitmapScaler> scaler;
winrt::check_hresult(m_wicFactory->CreateBitmapScaler(scaler.put()));
winrt::check_hresult(scaler->Initialize(
sourceBitmap.get(),
targetWidth,
targetHeight,
WICBitmapInterpolationModeHighQualityCubic));
finalSource = scaler;
OutputDebugStringW((L"Downsampled from " + std::to_wstring(frameDesc.Width) + L"x" + std::to_wstring(frameDesc.Height) +
L" to " + std::to_wstring(targetWidth) + L"x" + std::to_wstring(targetHeight) + L"\n").c_str());
}
// Use WriteSource - WIC will handle the BGRA to 8bpp indexed conversion
winrt::check_hresult(frameEncode->WriteSource(finalSource.get(), nullptr));
try
{
winrt::com_ptr<IWICMetadataQueryWriter> frameMetadataWriter;
if (SUCCEEDED(frameEncode->GetMetadataQueryWriter(frameMetadataWriter.put())) && frameMetadataWriter)
{
// Set the frame delay in the metadata (in hundredths of a second)
PROPVARIANT propValue;
PropVariantInit(&propValue);
propValue.vt = VT_UI2;
propValue.uiVal = static_cast<USHORT>(m_frameDelay);
frameMetadataWriter->SetMetadataByName(L"/grctlext/Delay", &propValue);
PropVariantClear(&propValue);
// Set disposal method (2 = restore to background, needed for animation)
PropVariantInit(&propValue);
propValue.vt = VT_UI1;
propValue.bVal = 2; // Disposal method: restore to background color
frameMetadataWriter->SetMetadataByName(L"/grctlext/Disposal", &propValue);
PropVariantClear(&propValue);
}
}
catch (...)
{
// Metadata setting failed, continue anyway
OutputDebugStringW(L"Warning: Failed to set GIF frame metadata\n");
}
// Commit the frame
OutputDebugStringW(L"About to commit frame to encoder...\n");
winrt::check_hresult(frameEncode->Commit());
OutputDebugStringW(L"Frame committed successfully\n");
// Unmap the staging texture
m_d3dContext->Unmap(stagingTexture.get(), 0);
// Increment and log frame count
m_frameCount++;
OutputDebugStringW((L"GIF Frame #" + std::to_wstring(m_frameCount) + L" fully encoded and committed\n").c_str());
return S_OK;
}
catch (const winrt::hresult_error& error)
{
OutputDebugStringW(error.message().c_str());
return error.code();
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::StartAsync
//
//----------------------------------------------------------------------------
winrt::IAsyncAction GifRecordingSession::StartAsync()
{
auto expected = false;
if (m_isRecording.compare_exchange_strong(expected, true))
{
auto self = shared_from_this();
try
{
// Start capturing frames
auto frameStartTime = std::chrono::high_resolution_clock::now();
int captureAttempts = 0;
int successfulCaptures = 0;
int duplicatedFrames = 0;
// Keep track of the last frame to duplicate when needed
winrt::com_ptr<ID3D11Texture2D> lastCroppedTexture;
while (m_isRecording && !m_closed)
{
captureAttempts++;
auto frame = m_frameWait->TryGetNextFrame();
winrt::com_ptr<ID3D11Texture2D> croppedTexture;
if (frame)
{
successfulCaptures++;
auto contentSize = frame->ContentSize;
auto frameTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame->FrameTexture);
D3D11_TEXTURE2D_DESC desc = {};
frameTexture->GetDesc(&desc);
// Use the smaller of the crop size or content size
auto width = min(m_rcCrop.right - m_rcCrop.left, contentSize.Width);
auto height = min(m_rcCrop.bottom - m_rcCrop.top, contentSize.Height);
D3D11_TEXTURE2D_DESC croppedDesc = {};
croppedDesc.Width = width;
croppedDesc.Height = height;
croppedDesc.MipLevels = 1;
croppedDesc.ArraySize = 1;
croppedDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
croppedDesc.SampleDesc.Count = 1;
croppedDesc.Usage = D3D11_USAGE_DEFAULT;
croppedDesc.BindFlags = D3D11_BIND_RENDER_TARGET;
winrt::check_hresult(m_d3dDevice->CreateTexture2D(&croppedDesc, nullptr, croppedTexture.put()));
// Set the content region to copy and clamp the coordinates
D3D11_BOX region = {};
region.left = std::clamp(m_rcCrop.left, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
region.right = std::clamp(m_rcCrop.left + width, static_cast<LONG>(0), static_cast<LONG>(desc.Width));
region.top = std::clamp(m_rcCrop.top, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
region.bottom = std::clamp(m_rcCrop.top + height, static_cast<LONG>(0), static_cast<LONG>(desc.Height));
region.back = 1;
// Copy the cropped region
m_d3dContext->CopySubresourceRegion(
croppedTexture.get(),
0,
0, 0, 0,
frameTexture.get(),
0,
&region);
// Save this as the last frame for duplication
lastCroppedTexture = croppedTexture;
}
else if (lastCroppedTexture)
{
// No new frame, duplicate the last one
duplicatedFrames++;
croppedTexture = lastCroppedTexture;
}
// Encode the frame (either new or duplicated)
if (croppedTexture)
{
HRESULT hr = EncodeFrame(croppedTexture.get());
if (FAILED(hr))
{
CloseInternal();
break;
}
}
// Wait for the next frame interval
co_await winrt::resume_after(std::chrono::milliseconds(1000 / m_frameRate));
}
// Commit the GIF encoder
if (m_gifEncoder)
{
auto frameEndTime = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(frameEndTime - frameStartTime).count();
OutputDebugStringW(L"Recording stopped. Committing GIF encoder...\n");
OutputDebugStringW((L"Total frames captured: " + std::to_wstring(m_frameCount) + L"\n").c_str());
OutputDebugStringW((L"Capture attempts: " + std::to_wstring(captureAttempts) + L"\n").c_str());
OutputDebugStringW((L"Successful captures: " + std::to_wstring(successfulCaptures) + L"\n").c_str());
OutputDebugStringW((L"Duplicated frames: " + std::to_wstring(duplicatedFrames) + L"\n").c_str());
OutputDebugStringW((L"Recording duration: " + std::to_wstring(duration) + L"ms\n").c_str());
OutputDebugStringW((L"Actual FPS: " + std::to_wstring(m_frameCount * 1000.0 / duration) + L"\n").c_str());
winrt::check_hresult(m_gifEncoder->Commit());
OutputDebugStringW(L"GIF encoder committed successfully\n");
}
}
catch (const winrt::hresult_error& error)
{
OutputDebugStringW(L"Error in GIF recording: ");
OutputDebugStringW(error.message().c_str());
OutputDebugStringW(L"\n");
// Try to commit the encoder even on error
if (m_gifEncoder)
{
try
{
m_gifEncoder->Commit();
}
catch (...) {}
}
CloseInternal();
}
}
co_return;
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::Close
//
//----------------------------------------------------------------------------
void GifRecordingSession::Close()
{
auto expected = false;
if (m_closed.compare_exchange_strong(expected, true))
{
expected = true;
if (!m_isRecording.compare_exchange_strong(expected, false))
{
CloseInternal();
}
else
{
m_frameWait->StopCapture();
}
}
}
//----------------------------------------------------------------------------
//
// GifRecordingSession::CloseInternal
//
//----------------------------------------------------------------------------
void GifRecordingSession::CloseInternal()
{
m_frameWait->StopCapture();
m_itemClosed.revoke();
}

View File

@@ -0,0 +1,69 @@
//==============================================================================
//
// Zoomit
// Sysinternals - www.sysinternals.com
//
// GIF recording support using Windows Imaging Component (WIC)
//
//==============================================================================
#pragma once
#include "CaptureFrameWait.h"
#include <d3d11_4.h>
#include <vector>
class GifRecordingSession : public std::enable_shared_from_this<GifRecordingSession>
{
public:
[[nodiscard]] static std::shared_ptr<GifRecordingSession> Create(
winrt::Direct3D11::IDirect3DDevice const& device,
winrt::GraphicsCaptureItem const& item,
RECT const& cropRect,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream);
~GifRecordingSession();
winrt::IAsyncAction StartAsync();
void EnableCursorCapture(bool enable = true) { m_frameWait->EnableCursorCapture(enable); }
void Close();
private:
GifRecordingSession(
winrt::Direct3D11::IDirect3DDevice const& device,
winrt::Capture::GraphicsCaptureItem const& item,
RECT const cropRect,
uint32_t frameRate,
winrt::Streams::IRandomAccessStream const& stream);
void CloseInternal();
HRESULT EncodeFrame(ID3D11Texture2D* texture);
private:
winrt::Direct3D11::IDirect3DDevice m_device{ nullptr };
winrt::com_ptr<ID3D11Device> m_d3dDevice;
winrt::com_ptr<ID3D11DeviceContext> m_d3dContext;
RECT m_rcCrop;
uint32_t m_frameRate;
winrt::GraphicsCaptureItem m_item{ nullptr };
winrt::GraphicsCaptureItem::Closed_revoker m_itemClosed;
std::shared_ptr<CaptureFrameWait> m_frameWait;
winrt::Streams::IRandomAccessStream m_stream{ nullptr };
// WIC components for GIF encoding
winrt::com_ptr<IWICImagingFactory> m_wicFactory;
winrt::com_ptr<IWICStream> m_wicStream;
winrt::com_ptr<IWICBitmapEncoder> m_gifEncoder;
winrt::com_ptr<IWICMetadataQueryWriter> m_encoderMetadataWriter;
std::atomic<bool> m_isRecording = false;
std::atomic<bool> m_closed = false;
uint32_t m_frameWidth=0;
uint32_t m_frameHeight=0;
uint32_t m_frameDelay=0;
uint32_t m_frameCount = 0;
int32_t m_width=0;
int32_t m_height=0;
};

View File

@@ -121,8 +121,8 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
BEGIN
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
LTEXT "ZoomIt v9.10",IDC_VERSION,42,7,73,10
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
LTEXT "ZoomIt v9.20",IDC_VERSION,42,7,73,10
LTEXT "Copyright <EFBFBD> 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
"SysLink",WS_TABSTOP,42,26,150,9
ICON "APPICON",IDC_STATIC,12,9,20,20
@@ -272,13 +272,15 @@ BEGIN
LTEXT "Note: Recording is only available on Windows 10 (version 1903) and higher.",IDC_STATIC,7,77,246,19
LTEXT "Scaling:",IDC_STATIC,30,115,26,8
COMBOBOX IDC_RECORD_SCALING,61,114,26,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | WS_VSCROLL | WS_TABSTOP
LTEXT "Format:",IDC_STATIC,30,132,26,8
COMBOBOX IDC_RECORD_FORMAT,61,131,60,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | WS_VSCROLL | WS_TABSTOP
LTEXT "Frame Rate:",IDC_STATIC,119,115,44,8,NOT WS_VISIBLE
COMBOBOX IDC_RECORD_FRAME_RATE,166,114,42,30,CBS_DROPDOWNLIST | CBS_AUTOHSCROLL | CBS_OEMCONVERT | CBS_SORT | NOT WS_VISIBLE | WS_VSCROLL | WS_TABSTOP
LTEXT "To crop the portion of the screen that will be recorded, enter the hotkey with the Shift key in the opposite mode. ",IDC_STATIC,7,32,246,19
LTEXT "To record a specific window, enter the hotkey with the Alt key in the opposite mode.",IDC_STATIC,7,55,246,19
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,137,83,10
COMBOBOX IDC_MICROPHONE,81,152,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_STATIC,32,154,47,8
CONTROL "&Capture audio input:",IDC_CAPTURE_AUDIO,"Button",BS_AUTOCHECKBOX | BS_LEFTTEXT | WS_TABSTOP,7,149,83,10
COMBOBOX IDC_MICROPHONE,81,164,172,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP
LTEXT "Microphone:",IDC_STATIC,32,166,47,8
END
SNIP DIALOGEX 0, 0, 260, 68

View File

@@ -234,6 +234,7 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="GifRecordingSession.cpp" />
<ClCompile Include="pch.cpp" />
<ClCompile Include="SelectRectangle.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Use</PrecompiledHeader>
@@ -288,6 +289,7 @@
<ClInclude Include="AudioSampleGenerator.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\Eula\Eula.h" />
<ClInclude Include="$(MSBuildThisFileDirectory)..\ZoomItModuleInterface\Trace.h" />
<ClInclude Include="GifRecordingSession.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Registry.h" />
<ClInclude Include="resource.h" />

View File

@@ -54,6 +54,9 @@
<ClCompile Include="$(MSBuildThisFileDirectory)..\..\..\common\sysinternals\WindowsVersions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="GifRecordingSession.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Registry.h">
@@ -95,6 +98,9 @@
<ClInclude Include="ZoomItSettings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="GifRecordingSession.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Image Include="appicon.ico">

View File

@@ -3,6 +3,13 @@
#include "Registry.h"
#include "DemoType.h"
// Recording format enum
enum class RecordingFormat
{
GIF = 0,
MP4 = 1
};
DWORD g_ToggleKey = (HOTKEYF_CONTROL << 8)| '1';
DWORD g_LiveZoomToggleKey = ((HOTKEYF_CONTROL) << 8)| '4';
DWORD g_DrawToggleKey = ((HOTKEYF_CONTROL) << 8)| '2';
@@ -38,8 +45,10 @@ BOOLEAN g_DemoTypeUserDriven = false;
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
DWORD g_RecordFrameRate = 30;
// Divide by 100 to get actual scaling
DWORD g_RecordScaling = 100;
DWORD g_RecordScalingGIF = 50;
DWORD g_RecordScalingMP4 = 100;
RecordingFormat g_RecordingFormat = RecordingFormat::GIF;
BOOLEAN g_CaptureAudio = FALSE;
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
@@ -79,7 +88,9 @@ REG_SETTING RegSettings[] = {
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
{ L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) },
{ L"RecordScaling", SETTING_TYPE_DWORD, 0, &g_RecordScaling, static_cast<DOUBLE>(g_RecordScaling) },
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(0) },
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
{ L"MicrophoneDeviceId", SETTING_TYPE_STRING, sizeof(g_MicrophoneDeviceId), g_MicrophoneDeviceId, static_cast<DOUBLE>(0) },
{ NULL, SETTING_TYPE_DWORD, 0, NULL, static_cast<DOUBLE>(0) }

View File

@@ -15,6 +15,7 @@
#include "Utility.h"
#include "WindowsVersions.h"
#include "ZoomItSettings.h"
#include "GifRecordingSession.h"
#ifdef __ZOOMIT_POWERTOYS__
#include <common/interop/shared_constants.h>
@@ -68,6 +69,8 @@ COLORREF g_CustomColors[16];
#define SNIP_SAVE_HOTKEY 9
#define DEMOTYPE_HOTKEY 10
#define DEMOTYPE_RESET_HOTKEY 11
#define RECORD_GIF_HOTKEY 12
#define RECORD_GIF_WINDOW_HOTKEY 13
#define ZOOM_PAGE 0
#define LIVE_PAGE 1
@@ -89,6 +92,11 @@ OPTION_TABS g_OptionsTabs[] = {
{ _T("Snip"), NULL }
};
static const TCHAR* g_RecordingFormats[] = {
_T("GIF"),
_T("MP4")
};
float g_ZoomLevels[] = {
1.25,
1.50,
@@ -99,6 +107,8 @@ float g_ZoomLevels[] = {
};
DWORD g_FramerateOptions[] = {
15,
24,
30,
60
};
@@ -152,12 +162,15 @@ BOOLEAN g_running = TRUE;
// Screen recording globals
#define DEFAULT_RECORDING_FILE L"Recording.mp4"
#define DEFAULT_GIF_RECORDING_FILE L"Recording.gif"
BOOL g_RecordToggle = FALSE;
BOOL g_RecordCropping = FALSE;
SelectRectangle g_SelectRectangle;
std::wstring g_RecordingSaveLocation;
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
type_pGetMonitorInfo pGetMonitorInfo;
type_MonitorFromPoint pMonitorFromPoint;
@@ -1771,6 +1784,58 @@ INT_PTR CALLBACK OptionsTabProc( HWND hDlg, UINT message,
case WM_INITDIALOG:
return TRUE;
case WM_COMMAND:
// Handle combo box selection changes
if (HIWORD(wParam) == CBN_SELCHANGE) {
if (LOWORD(wParam) == IDC_RECORD_SCALING) {
int format = static_cast<int>(SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT), CB_GETCURSEL, 0, 0));
int scale = static_cast<int>(SendMessage(GetDlgItem(hDlg, IDC_RECORD_SCALING), CB_GETCURSEL, 0, 0));
if(format == 0)
{
g_RecordScalingGIF = static_cast<BYTE>((scale + 1) * 10);
}
else
{
g_RecordScalingMP4 = static_cast<BYTE>((scale + 1) * 10);
}
}
else if (LOWORD(wParam) == IDC_RECORD_FORMAT) {
// Get the currently selected format
int selection = static_cast<int>(SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT),
CB_GETCURSEL, 0, 0));
// Get the selected text to check if it's GIF
TCHAR selectedText[32] = {0};
SendMessage(GetDlgItem(hDlg, IDC_RECORD_FORMAT),
CB_GETLBTEXT, selection, reinterpret_cast<LPARAM>(selectedText));
// Check if GIF is selected by comparing the text
bool isGifSelected = (wcscmp(selectedText, L"GIF") == 0);
// if gif is selected set the scaling to the g_recordScaleGIF value otherwise to the g_recordScaleMP4 value
if (isGifSelected) {
g_RecordScaling = g_RecordScalingGIF;
} else {
g_RecordScaling = g_RecordScalingMP4;
}
for (int i = 0; i < 10; i++) {
int scalingValue = (i + 1) * 10;
if (scalingValue == static_cast<int>(g_RecordScaling)) {
SendMessage(GetDlgItem(hDlg, IDC_RECORD_SCALING),
CB_SETCURSEL, i, 0);
break;
}
}
// Enable/disable microphone controls based on selection
EnableWindow(GetDlgItem(hDlg, IDC_MICROPHONE), !isGifSelected);
EnableWindow(GetDlgItem(hDlg, IDC_CAPTURE_AUDIO), !isGifSelected);
}
}
switch ( LOWORD( wParam )) {
case IDC_ADVANCED_BREAK:
DialogBox( g_hInstance, L"ADVANCED_BREAK", hDlg, AdvancedBreakProc );
@@ -1914,6 +1979,8 @@ void UnregisterAllHotkeys( HWND hWnd )
UnregisterHotKey( hWnd, SNIP_SAVE_HOTKEY);
UnregisterHotKey( hWnd, DEMOTYPE_HOTKEY );
UnregisterHotKey( hWnd, DEMOTYPE_RESET_HOTKEY );
UnregisterHotKey( hWnd, RECORD_GIF_HOTKEY );
UnregisterHotKey( hWnd, RECORD_GIF_WINDOW_HOTKEY );
}
//----------------------------------------------------------------------------
@@ -1943,6 +2010,9 @@ void RegisterAllHotkeys(HWND hWnd)
RegisterHotKey(hWnd, RECORD_CROP_HOTKEY, (g_RecordToggleMod ^ MOD_SHIFT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF);
RegisterHotKey(hWnd, RECORD_WINDOW_HOTKEY, (g_RecordToggleMod ^ MOD_ALT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF);
}
// Register CTRL+8 for GIF recording and CTRL+ALT+8 for GIF window recording
RegisterHotKey(hWnd, RECORD_GIF_HOTKEY, MOD_CONTROL | MOD_NOREPEAT, 568 && 0xFF);
RegisterHotKey(hWnd, RECORD_GIF_WINDOW_HOTKEY, MOD_CONTROL | MOD_ALT | MOD_NOREPEAT, 568 && 0xFF);
}
@@ -2113,12 +2183,33 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0));
}
}
// Add the recording format to the combo box and set the current selection
size_t selection = 0;
const wchar_t* currentFormatString = (g_RecordingFormat == RecordingFormat::GIF) ? L"GIF" : L"MP4";
for( size_t i = 0; i < (sizeof(g_RecordingFormats) / sizeof(g_RecordingFormats[0])); i++ )
{
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(g_RecordingFormats[i]) );
if( selection == 0 && wcscmp( g_RecordingFormats[i], currentFormatString ) == 0 )
{
selection = i;
}
}
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT ), CB_SETCURSEL, static_cast<WPARAM>(selection), static_cast<LPARAM>(0) );
for(unsigned int i = 1; i < 11; i++) {
_stprintf(text, L"%2.1f", (static_cast<double>(i)) / 10 );
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_ADDSTRING),
static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(text));
if (g_RecordScaling == i*10 ) {
if (g_RecordingFormat == RecordingFormat::GIF && i*10 == g_RecordScalingGIF ) {
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), CB_SETCURSEL, static_cast<WPARAM>(i)-1, static_cast<LPARAM>(0));
}
if (g_RecordingFormat == RecordingFormat::MP4 && i*10 == g_RecordScalingMP4 ) {
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), CB_SETCURSEL, static_cast<WPARAM>(i)-1, static_cast<LPARAM>(0));
}
@@ -2136,7 +2227,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
// Add the microphone devices to the combo box and set the current selection
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(L"Default"));
size_t selection = 0;
selection = 0;
for( size_t i = 0; i < microphones.size(); i++ )
{
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), static_cast<UINT>(CB_ADDSTRING), static_cast<WPARAM>(0), reinterpret_cast<LPARAM>(microphones[i].second.c_str()) );
@@ -2147,6 +2238,11 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
}
SendMessage( GetDlgItem( g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE ), CB_SETCURSEL, static_cast<WPARAM>(selection), static_cast<LPARAM>(0) );
// Set initial state of microphone controls based on recording format
bool isGifSelected = (g_RecordingFormat == RecordingFormat::GIF);
EnableWindow(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_MICROPHONE), !isGifSelected);
EnableWindow(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO), !isGifSelected);
if( GetFileAttributes( g_DemoTypeFile ) == -1 )
{
memset( g_DemoTypeFile, 0, sizeof( g_DemoTypeFile ) );
@@ -2249,7 +2345,17 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
text[2] = 0;
newTimeout = _tstoi( text );
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))];
if( g_RecordingFormat == RecordingFormat::GIF )
{
// Hardcode lower frame rate for GIFs
g_RecordFrameRate = 15;
}
else
{
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))];
}
g_RecordingFormat = static_cast<RecordingFormat>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)));
g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10);
// Get the selected microphone
@@ -3395,6 +3501,12 @@ void StopRecording()
g_RecordingSession = nullptr;
}
if ( g_GifRecordingSession != nullptr ) {
g_GifRecordingSession->Close();
g_GifRecordingSession = nullptr;
}
g_RecordToggle = FALSE;
#if WINDOWS_CURSOR_RECORDING_WORKAROUND
@@ -3451,7 +3563,10 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
auto tempFolderPath = std::filesystem::temp_directory_path().wstring();
auto tempFolder = co_await winrt::StorageFolder::GetFolderFromPathAsync( tempFolderPath );
auto appFolder = co_await tempFolder.CreateFolderAsync( L"ZoomIt", winrt::CreationCollisionOption::OpenIfExists );
auto file = co_await appFolder.CreateFileAsync( L"zoomit.mp4", winrt::CreationCollisionOption::ReplaceExisting );
// Choose temp file extension based on format
const wchar_t* tempFileName = (g_RecordingFormat == RecordingFormat::GIF) ? L"zoomit.gif" : L"zoomit.mp4";
auto file = co_await appFolder.CreateFileAsync( tempFileName, winrt::CreationCollisionOption::ReplaceExisting );
// Get the device
auto d3dDevice = util::CreateD3D11Device();
@@ -3474,21 +3589,40 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
item = util::CreateCaptureItemForMonitor( hMon );
auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite );
g_RecordingSession = VideoRecordingSession::Create(
g_RecordDevice,
item,
*rcCrop,
g_RecordFrameRate,
g_CaptureAudio,
stream );
if( g_hWndLiveZoom != NULL )
g_RecordingSession->EnableCursorCapture( false );
// Create the appropriate recording session based on format
if (g_RecordingFormat == RecordingFormat::GIF)
{
g_GifRecordingSession = GifRecordingSession::Create(
g_RecordDevice,
item,
*rcCrop,
g_RecordFrameRate,
stream );
co_await g_RecordingSession->StartAsync();
if( g_hWndLiveZoom != NULL )
g_GifRecordingSession->EnableCursorCapture( false );
// g_RecordingSession isn't null if we're aborting a recording
if( g_RecordingSession == nullptr ) {
co_await g_GifRecordingSession->StartAsync();
}
else
{
g_RecordingSession = VideoRecordingSession::Create(
g_RecordDevice,
item,
*rcCrop,
g_RecordFrameRate,
g_CaptureAudio,
stream );
if( g_hWndLiveZoom != NULL )
g_RecordingSession->EnableCursorCapture( false );
co_await g_RecordingSession->StartAsync();
}
// Check if recording was aborted
if( g_RecordingSession == nullptr && g_GifRecordingSession == nullptr ) {
g_bSaveInProgress = true;
@@ -3504,11 +3638,24 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
wil::com_ptr<IShellItem> videosItem;
if( SUCCEEDED ( SHGetKnownFolderItem( FOLDERID_Videos, KF_FLAG_DEFAULT, nullptr, IID_IShellItem, (void**) videosItem.put() ) ) )
saveDialog->SetDefaultFolder( videosItem.get() );
saveDialog->SetDefaultExtension( L".mp4" );
COMDLG_FILTERSPEC fileTypes[] = {
{ L"MP4 Video", L"*.mp4" }
};
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
// Set file type based on the recording format
if (g_RecordingFormat == RecordingFormat::GIF)
{
saveDialog->SetDefaultExtension( L".gif" );
COMDLG_FILTERSPEC fileTypes[] = {
{ L"GIF Animation", L"*.gif" }
};
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
}
else
{
saveDialog->SetDefaultExtension( L".mp4" );
COMDLG_FILTERSPEC fileTypes[] = {
{ L"MP4 Video", L"*.mp4" }
};
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
}
if( g_RecordingSaveLocation.size() == 0) {
@@ -3516,8 +3663,12 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
wil::unique_cotaskmem_string folderPath;
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put())))
g_RecordingSaveLocation = folderPath.get();
g_RecordingSaveLocation = std::filesystem::path{ g_RecordingSaveLocation } /= DEFAULT_RECORDING_FILE;
}
// Always use appropriate default filename based on current format
std::filesystem::path currentPath{ g_RecordingSaveLocation };
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF) ? DEFAULT_GIF_RECORDING_FILE : DEFAULT_RECORDING_FILE;
g_RecordingSaveLocation = currentPath.parent_path() / defaultFile;
auto suggestedName = GetUniqueRecordingFilename();
saveDialog->SetFileName( suggestedName.c_str() );
@@ -3566,6 +3717,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
}
co_await file.DeleteAsync();
g_RecordingSession = nullptr;
g_GifRecordingSession = nullptr;
}
} catch( const winrt::hresult_error& error ) {
@@ -3792,6 +3944,13 @@ LRESULT APIENTRY MainWndProc(
reg.ReadRegSettings( RegSettings );
// Set g_RecordScaling based on the current recording format
if (g_RecordingFormat == RecordingFormat::GIF) {
g_RecordScaling = g_RecordScalingGIF;
} else {
g_RecordScaling = g_RecordScalingMP4;
}
// to support migrating from
if ((g_PenColor >> 24) == 0) {
g_PenColor |= 0xFF << 24;
@@ -4206,6 +4365,8 @@ LRESULT APIENTRY MainWndProc(
case RECORD_HOTKEY:
case RECORD_CROP_HOTKEY:
case RECORD_WINDOW_HOTKEY:
case RECORD_GIF_HOTKEY:
case RECORD_GIF_WINDOW_HOTKEY:
//
// Recording
@@ -4335,7 +4496,7 @@ LRESULT APIENTRY MainWndProc(
cropRc = {};
// if we're recording a window, get the window
if (wParam == RECORD_WINDOW_HOTKEY)
if (wParam == RECORD_WINDOW_HOTKEY || wParam == RECORD_GIF_WINDOW_HOTKEY)
{
GetCursorPos(&cursorPos);
hWndRecord = WindowFromPoint(cursorPos);
@@ -4353,6 +4514,7 @@ LRESULT APIENTRY MainWndProc(
if( g_RecordToggle == FALSE )
{
g_RecordToggle = TRUE;
#ifdef __ZOOMIT_POWERTOYS__
if( g_StartedByPowerToys )
{
@@ -6147,6 +6309,13 @@ LRESULT APIENTRY MainWndProc(
showOptions = TRUE;
}
}
// Register CTRL+8 for GIF recording and CTRL+ALT+8 for GIF window recording
if (!RegisterHotKey(hWnd, RECORD_GIF_HOTKEY, MOD_CONTROL | MOD_NOREPEAT, '8') ||
!RegisterHotKey(hWnd, RECORD_GIF_WINDOW_HOTKEY, MOD_CONTROL | MOD_ALT | MOD_NOREPEAT, '8'))
{
MessageBox(hWnd, L"The specified GIF recording hotkey is already in use.\nSelect a different GIF recording hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
if (showOptions)
{
// To open the PowerToys settings in the ZoomIt page.

View File

@@ -93,6 +93,7 @@
#define IDC_DEMOTYPE_SLIDER2 1074
#define IDC_DEMOTYPE_STATIC2 1074
#define IDC_COPYRIGHT 1075
#define IDC_RECORD_FORMAT 1076
#define IDC_PEN_WIDTH 1105
#define IDC_TIMER 1106
#define IDC_SMOOTH_IMAGE 1107

View File

@@ -14,6 +14,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
const unsigned int SPECIAL_SEMANTICS_SHORTCUT = 1;
const unsigned int SPECIAL_SEMANTICS_COLOR = 2;
const unsigned int SPECIAL_SEMANTICS_LOG_FONT = 3;
const unsigned int SPECIAL_SEMANTICS_RECORDING_FORMAT = 4;
const unsigned int SPECIAL_SEMANTICS_RECORD_SCALING_GIF = 5;
const unsigned int SPECIAL_SEMANTICS_RECORD_SCALING_MP4 = 6;
std::vector<unsigned char> base64_decode(const std::wstring& base64_string)
{
@@ -72,6 +75,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
{ L"PenColor", SPECIAL_SEMANTICS_COLOR },
{ L"BreakPenColor", SPECIAL_SEMANTICS_COLOR },
{ L"Font", SPECIAL_SEMANTICS_LOG_FONT },
{ L"RecordingFormat", SPECIAL_SEMANTICS_RECORDING_FORMAT },
{ L"RecordScalingGIF", SPECIAL_SEMANTICS_RECORD_SCALING_GIF },
{ L"RecordScalingMP4", SPECIAL_SEMANTICS_RECORD_SCALING_MP4 },
};
hstring ZoomItSettings::LoadSettingsJson()
@@ -103,6 +109,11 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
value & 0xFF);
_settings.add_property(curSetting->ValueName, hotkey.get_json());
}
else if (special_semantics->second == SPECIAL_SEMANTICS_RECORDING_FORMAT)
{
std::wstring formatString = (value == 0) ? L"GIF" : L"MP4";
_settings.add_property(L"RecordFormat", formatString);
}
else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR)
{
/* PowerToys settings likes colors as #FFFFFF strings.
@@ -156,6 +167,9 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
curSetting++;
}
DWORD recordScaling = (g_RecordingFormat == static_cast<RecordingFormat>(0)) ? g_RecordScalingGIF : g_RecordScalingMP4;
_settings.add_property<DWORD>(L"RecordScaling", recordScaling);
return _settings.get_raw_json().Stringify();
}
@@ -167,6 +181,8 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
PowerToysSettings::PowerToyValues valuesFromSettings =
PowerToysSettings::PowerToyValues::from_json_string(json, L"ZoomIt");
bool formatChanged = false;
PREG_SETTING curSetting = RegSettings;
while (curSetting->ValueName)
{
@@ -212,6 +228,42 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
*static_cast<PDWORD>(curSetting->Setting) = value;
}
}
else if (special_semantics->second == SPECIAL_SEMANTICS_RECORDING_FORMAT)
{
// Convert string ("GIF" or "MP4") to DWORD enum value (0=GIF, 1=MP4)
auto possibleValue = valuesFromSettings.get_string_value(L"RecordFormat");
if (possibleValue.has_value())
{
RecordingFormat oldFormat = g_RecordingFormat;
DWORD formatValue = (possibleValue.value() == L"GIF") ? 0 : 1;
RecordingFormat newFormat = static_cast<RecordingFormat>(formatValue);
*static_cast<PDWORD>(curSetting->Setting) = formatValue;
if (oldFormat != newFormat)
{
formatChanged = true;
if (oldFormat == static_cast<RecordingFormat>(0))
{
g_RecordScalingGIF = g_RecordScaling;
}
else
{
g_RecordScalingMP4 = g_RecordScaling;
}
if (newFormat == static_cast<RecordingFormat>(0))
{
g_RecordScaling = g_RecordScalingGIF;
}
else
{
g_RecordScaling = g_RecordScalingMP4;
}
}
}
}
else if (special_semantics->second == SPECIAL_SEMANTICS_COLOR)
{
/* PowerToys settings likes colors as #FFFFFF strings.
@@ -275,6 +327,22 @@ namespace winrt::PowerToys::ZoomItSettingsInterop::implementation
}
curSetting++;
}
auto recordScalingValue = valuesFromSettings.get_uint_value(L"RecordScaling");
if (recordScalingValue.has_value() && !formatChanged)
{
g_RecordScaling = recordScalingValue.value();
if (g_RecordingFormat == static_cast<RecordingFormat>(0))
{
g_RecordScalingGIF = recordScalingValue.value();
}
else
{
g_RecordScalingMP4 = recordScalingValue.value();
}
}
reg.WriteRegSettings(RegSettings);
}
}

View File

@@ -87,6 +87,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public IntProperty RecordScaling { get; set; }
public StringProperty RecordFormat { get; set; }
public BoolProperty CaptureAudio { get; set; }
public StringProperty MicrophoneDeviceId { get; set; }

View File

@@ -241,6 +241,12 @@
<ComboBoxItem>1.0</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordFormat" x:Uid="ZoomIt_Record_Format">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.RecordFormatIndex, Mode=TwoWay}">
<ComboBoxItem>GIF</ComboBoxItem>
<ComboBoxItem>MP4</ComboBoxItem>
</ComboBox>
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ZoomItRecordCaptureAudio" x:Uid="ZoomIt_Record_CaptureAudio">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.RecordCaptureAudio, Mode=TwoWay}" />
</tkcontrols:SettingsCard>

View File

@@ -5046,6 +5046,9 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="ZoomIt_Record_Scaling.Header" xml:space="preserve">
<value>Scaling</value>
</data>
<data name="ZoomIt_Record_Format.Header" xml:space="preserve">
<value>Format</value>
</data>
<data name="ZoomIt_Record_CaptureAudio.Header" xml:space="preserve">
<value>Capture audio input</value>
</data>

View File

@@ -652,6 +652,54 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public int RecordFormatIndex
{
get
{
if (_zoomItSettings.Properties.RecordFormat.Value == "GIF")
{
return 0;
}
if (_zoomItSettings.Properties.RecordFormat.Value == "MP4")
{
return 1;
}
return 0;
}
set
{
int format = 0;
if (_zoomItSettings.Properties.RecordFormat.Value == "GIF")
{
format = 0;
}
if (_zoomItSettings.Properties.RecordFormat.Value == "MP4")
{
format = 1;
}
if (format != value)
{
_zoomItSettings.Properties.RecordFormat.Value = value == 0 ? "GIF" : "MP4";
OnPropertyChanged(nameof(RecordFormatIndex));
NotifySettingsChanged();
// Reload settings to get the new format's scaling value
var reloadedSettings = global::PowerToys.ZoomItSettingsInterop.ZoomItSettings.LoadSettingsJson();
var reloaded = JsonSerializer.Deserialize<ZoomItSettings>(reloadedSettings, _serializerOptions);
if (reloaded != null && reloaded.Properties != null)
{
_zoomItSettings.Properties.RecordScaling.Value = reloaded.Properties.RecordScaling.Value;
OnPropertyChanged(nameof(RecordScalingIndex));
}
}
}
}
public bool RecordCaptureAudio
{
get => _zoomItSettings.Properties.CaptureAudio.Value;