Skip to content

Instantly share code, notes, and snippets.

@SoylentGraham
Created June 20, 2016 14:15
Show Gist options
  • Save SoylentGraham/fd9b434c35cbeb43d55add2b81afb542 to your computer and use it in GitHub Desktop.
Save SoylentGraham/fd9b434c35cbeb43d55add2b81afb542 to your computer and use it in GitHub Desktop.
Windows Window capture in PopMovie.xyz
#include "HwndExtractor.h"
#include <future>
#include <SoyJson.h>
void EnumWindows(ArrayBridge<TWindowHandle>&& Handles,std::function<bool(const std::string&)> Filter)
{
// WNDENUMPROC
auto EnumWindowsCallback = [](HWND window_handle, LPARAM param) -> BOOL
{
auto& HandlesArray = *(ArrayBridge<TWindowHandle>*)param;
if ( !IsWindowVisible(window_handle) )
return TRUE;
RECT rectangle = {0};
GetWindowRect(window_handle, &rectangle);
if (IsRectEmpty(&rectangle))
return TRUE;
char window_title[256];
GetWindowText(window_handle, window_title, sizeof(window_title) );
if(strlen(window_title) == 0)
return TRUE;
if(strcmp(window_title, "Program Manager")==0)
return TRUE;
TWindowHandle Handle;
Handle.mHandle = window_handle;
Handle.mName = window_title;
Handle.mRect = rectangle;
HandlesArray.PushBack( Handle );
return TRUE;
};
// just make sure the cast is okay
ArrayBridge<TWindowHandle>* pHandles = &Handles;
::EnumWindows( EnumWindowsCallback, (LPARAM)pHandles );
for ( int i=Handles.GetSize()-1; i>=0; i-- )
{
if ( Filter( Handles[i].mName ) )
continue;
Handles.RemoveBlock( i, 1 );
}
}
const char* EnumToString(const std::future_status& in)
{
switch (in)
{
case std::future_status::deferred: return "deffered";
case std::future_status::timeout: return "timeout";
case std::future_status::ready: return "ready";
default: return "unknown";
}
}
void Hwnd::EnumWindows(std::function<void(const std::string&)> AppendName,std::function<bool()> Block)
{
// if this async is run on another thread when we abort, we want that thread to know safely to not do anything
std::shared_ptr<bool> Aborted( new bool );
*Aborted = false;
// VERY important that Aborted is a copy!
auto EnumAsync = [&AppendName,Aborted]
{
auto WindowFilter = [](const std::string& Name)
{
// skip empty window names - later we'll handle this
if ( Name.empty() )
return false;
return true;
};
Array<TWindowHandle> Handles;
EnumWindows( GetArrayBridge(Handles), WindowFilter );
// outside thread has been aborted, AppendName lambda is now invalid!
if ( *Aborted )
return;
for ( int h=0; h<Handles.GetSize(); h++ )
{
auto& Handle = Handles[h];
AppendName( Handle.mName );
}
};
// gr: this (::EnumWindows) can cause a deadlock if TSourceManager is waiting for the thread to finish (inside WNdProc)
// so run it in an async and if it seems to be deadlocking... keep checking if we want to abort
auto Future = std::async( EnumAsync );
while ( Block() )
{
// gr: this may stall app shutdown, but that's better than reporting debug
auto Status = Future.wait_for( std::chrono::milliseconds(400) );
// finished
if ( Status == std::future_status::ready )
break;
// otherwise timeout still going, or hasn't started yet...
static bool DebugWaitingOnFuture = true;
if ( DebugWaitingOnFuture )
std::Debug << "Still waiting for EnumWindows future..." << EnumToString(Status) << std::endl;
}
// in case we've left the enum running on an async thread, let it know not to do anything
*Aborted = true;
}
SoyPixelsFormat::Type GetFormat(DWORD BitmapCompression,WORD BitCount)
{
switch ( BitmapCompression )
{
case BI_RGB:
case BI_BITFIELDS:
{
if ( BitCount == 32 )
return SoyPixelsFormat::BGRA;
if ( BitCount == 24 )
return SoyPixelsFormat::BGR;
}
break;
default:
break;
}
std::stringstream Error;
Error << "Unsupported bitmap format " << BitmapCompression << " (" << BitCount << " bit)";
throw Soy::AssertException( Error.str() );
}
vec2x<size_t> GetDcSize(HDC Handle)
{
BITMAP BitmapHeader;
memset( &BitmapHeader, 0, sizeof(BitmapHeader) );
HGDIOBJ hBitmap = GetCurrentObject( Handle, OBJ_BITMAP );
auto Result = GetObject( hBitmap, sizeof(BitmapHeader), &BitmapHeader );
return vec2x<size_t>( BitmapHeader.bmWidth, BitmapHeader.bmHeight );
}
void ReadWindowPixels(TWindowHandle Handle,SoyPixelsImpl& Pixels,bool ClientAreaOnly,float3x3& Transform,vec2x<int>& WindowPos)
{
// flush last error
::Platform::FlushLastError();
static int TotalTimerMin = 40;
// typically around 16ms it seems (matching monitor refresh rate?)
// gr: with the win81 fast copy, PrintWindow seems to take a little bit longer...
// gr: increased, takes about 28ms on windows81 desktop machine...
static int TimerMin = 30;
Soy::TScopeTimerPrint Timer("Read window pixels", TotalTimerMin );
Soy::TScopeTimerPrint Timer_a("get dc's", TimerMin);
HDC window_dc = GetWindowDC(Handle.mHandle);
HDC global_dc = GetDC(0);
Timer_a.Stop();
if ( !window_dc && !global_dc )
throw Soy::AssertException("Failed to get device context");
// make a temp dc to copy to
Soy::TScopeTimerPrint Timer_b("CreateCompatibleDC", TimerMin);
HDC temp_dc = CreateCompatibleDC( window_dc );
Timer_b.Stop();
if ( !temp_dc )
throw Soy::AssertException("Failed to create a temporary dc");
// grab latest rect
Soy::TScopeTimerPrint Timer_c("GetWindowRect", TimerMin);
RECT window_rectangle;
if ( ClientAreaOnly )
{
if ( !GetClientRect( Handle.mHandle, &window_rectangle ) )
throw Soy::AssertException("Failed to create a temporary dc");
}
else
{
if ( !GetWindowRect( Handle.mHandle, &window_rectangle ) )
throw Soy::AssertException("Failed to create a temporary dc");
}
Timer_c.Stop();
WindowPos.x = window_rectangle.left;
WindowPos.y = window_rectangle.top;
auto Width = window_rectangle.right - window_rectangle.left;
auto Height = window_rectangle.bottom - window_rectangle.top;
auto OldWidth = Width;
auto OldHeight = Height;
// gr: docs say scanlines need to align, but seems to be corrupted (win8.1) if height isn't aligned either
// gr: alignment seems to need to be 32, not 16 on win7
static int WidthAlignment = 32;
static int HeightAlignment = 32;
static bool Crop = false; // else pad
if ( WidthAlignment != 0 && Crop )
Width -= Width % WidthAlignment;
if ( WidthAlignment != 0 && !Crop )
Width += WidthAlignment - (Width % WidthAlignment);
if ( HeightAlignment != 0 && Crop )
Height -= Height % HeightAlignment;
if ( HeightAlignment != 0 && !Crop )
Height += HeightAlignment - (Height % WidthAlignment);
// make up transform for clipping
Transform( 0,0 ) = OldWidth / static_cast<float>(Width);
Transform( 1,1 ) = OldHeight / static_cast<float>(Height);
Soy::TScopeTimerPrint Timer_d("CreateCompatibleBitmap", TimerMin);
HBITMAP bitmap = CreateCompatibleBitmap( window_dc, Width, Height );
Soy::Assert( bitmap!=nullptr, "Failed to get bitmap");
Timer_d.Stop();
BITMAPINFO BitmapInfo;
memset( &BitmapInfo, 0, sizeof(BitmapInfo) );
BitmapInfo.bmiHeader.biSize = sizeof(BitmapInfo);
int x = 0;
// grab current bitmap info
{
Soy::TScopeTimerPrint Timer_e("GetDIBits meta", TimerMin);
auto InitResult = GetDIBits( temp_dc, bitmap, x, Height, nullptr, &BitmapInfo, DIB_RGB_COLORS );
if ( InitResult == 0 )
{
std::Debug << "GetDibBits meta returned zero (error)" << std::endl;
}
}
static bool OverwriteOutputBmp = true;
if ( OverwriteOutputBmp )
{
BitmapInfo.bmiHeader.biPlanes = 1;
// gr: 24 bit seems to give us nonsense... or it's still BGRA
BitmapInfo.bmiHeader.biBitCount = 32;
BitmapInfo.bmiHeader.biCompression = BI_RGB;
BitmapInfo.bmiHeader.biSizeImage = 0;
// flip image
// gr: when?..
static bool Flip = true;
if ( Flip )
{
BitmapInfo.bmiHeader.biHeight = - BitmapInfo.bmiHeader.biHeight;
}
}
// alloc pixels THEN say how much we need for the bitmap read
Soy::TScopeTimerPrint Timer_h("Pixels alloc", TimerMin);
Pixels.Init( Width, Height, GetFormat( BitmapInfo.bmiHeader.biCompression, BitmapInfo.bmiHeader.biBitCount ) );
auto& PixelsArray = Pixels.GetPixelsArray();
BitmapInfo.bmiHeader.biSizeImage = PixelsArray.GetDataSize();
Timer_h.Stop();
Soy::Assert( PixelsArray.GetDataSize() == BitmapInfo.bmiHeader.biSizeImage, "Pixel size mismatch" );
// select bitmap to write to
Soy::TScopeTimerPrint Timer_g("SelectObject", TimerMin);
auto ObjectHandle = SelectObject( temp_dc, bitmap );
if ( ObjectHandle == nullptr )
{
// selected object is not a region
throw Soy::AssertException("Select object failed, not a region");
}
if ( ObjectHandle == HGDI_ERROR )
{
std::stringstream Error;
Error << "SelectObject failed (HGDI_ERROR), last error: " << ::Platform::GetLastErrorString();
throw Soy::AssertException( Error.str() );
}
Timer_g.Stop();
// gr: in comments, this suggests it might give a printer-style output
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162869(v=vs.85).aspx
UINT Flags = ClientAreaOnly ? PW_CLIENTONLY : 0;
// from comments; faster non-flicker mode for windows 8.1
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162869(v=vs.85).aspx
// gr: need explicit windows version check, doesnt work on win7
#if(_WIN32_WINNT >= 0x0603)
static bool Win81FastCopy = true;
if ( Win81FastCopy )
Flags |= PW_RENDERFULLCONTENT;
#endif
Soy::TScopeTimerPrint Timer_i("PrintWindow", TimerMin);
auto PrintSuccess = PrintWindow( Handle.mHandle, temp_dc, Flags );
if ( !PrintSuccess && bool_cast(Flags & PW_RENDERFULLCONTENT) )
{
// if we get this error, we're probably on win7. maybe pre-empt this
if ( Platform::GetLastError(false) == ERROR_INVALID_PARAMETER )
{
auto NewFlags = Flags & ~PW_RENDERFULLCONTENT;
PrintSuccess = PrintWindow( Handle.mHandle, temp_dc, NewFlags );
}
}
if ( !PrintSuccess )
{
std::stringstream Error;
Error << "PrintWindow failed, last error: " << Platform::GetLastErrorString();
throw Soy::AssertException( Error.str() );
}
Timer_i.Stop();
auto* Bytes = PixelsArray.GetArray();
Soy::TScopeTimerPrint Timer_e("GetDIBits", TimerMin);
int LinesToCopy = Height;
auto LinesCopied = GetDIBits( temp_dc, bitmap, x, LinesToCopy, Bytes, &BitmapInfo, DIB_RGB_COLORS );
if ( LinesCopied == 0 )
{
std::Debug << "Failed to copy any lines; last error: " << Platform::GetLastErrorString() << std::endl;
}
Timer_e.Stop();
Soy::TScopeTimerPrint Timer_f("cleanup", TimerMin);
DeleteObject( bitmap );
DeleteDC( temp_dc );
ReleaseDC( Handle.mHandle, window_dc );
Timer_f.Stop();
if ( LinesCopied == 0 )
throw Soy::AssertException("Failed to copy any bitmap lines");
}
HwndExtractor::HwndExtractor(const TMediaExtractorParams& Params) :
TMediaExtractor ( Params ),
mWindowTitle ( Params.mFilename )
{
// find handle
mWindowHandle = GetWindowHandle();
Start();
}
void HwndExtractor::GetStreams(ArrayBridge<TStreamMeta>&& Streams)
{
TStreamMeta Meta;
Meta.mStreamIndex = 0;
Meta.mCodec = SoyMediaFormat::RGB;
Streams.PushBack( Meta );
}
TWindowHandle HwndExtractor::GetWindowHandle()
{
std::Debug << __func__ << " " << mWindowTitle << std::endl;
// grab a raw copy of the pixels
auto WindowFilter = [this](const std::string& Name)
{
if ( Name == "*" )
return true;
if ( !Soy::StringContains( Name, mWindowTitle, false ) )
return false;
return true;
};
Array<TWindowHandle> Handles;
EnumWindows( GetArrayBridge(Handles), WindowFilter );
if ( Handles.IsEmpty() )
{
std::stringstream Error;
Error << "Could not find window named " << mWindowTitle;
throw Soy::AssertException( Error.str() );
}
// work out best match
return Handles[0];
}
std::shared_ptr<TMediaPacket> HwndExtractor::ReadNextPacket()
{
// re-fetch handle if we need to
if ( !mWindowHandle.IsValid() )
mWindowHandle = GetWindowHandle();
// grab pixels
SoyPixels Pixels;
float3x3 Transform;
vec2x<int> WindowPos;
ReadWindowPixels( mWindowHandle, Pixels, !mParams.mWindowIncludeBorders, Transform, WindowPos );
// failed to get pixels (without error)
// gr: record this as dropped
if ( !Pixels.IsValid() )
return nullptr;
// gr: whilst we don't have dx transform, do a quick clip
static bool DoCpuClip = true;
if ( DoCpuClip )
{
auto mApplyWidthPadding = mParams.mApplyHeightPadding;
auto ClippedWidth = mApplyWidthPadding ? Pixels.GetWidth() * Transform(0,0) : Pixels.GetWidth();
auto ClippedHeight = mParams.mApplyHeightPadding ? Pixels.GetHeight() * Transform(1,1) : Pixels.GetHeight();
Pixels.ResizeClip( ClippedWidth, ClippedHeight );
Transform = float3x3();
}
// make a packet
std::shared_ptr<TMediaPacket> pPacket( new TMediaPacket );
auto& Packet = *pPacket;
Packet.mMeta.mCodec = SoyMediaFormat::FromPixelFormat( Pixels.GetFormat() );
Packet.mMeta.mPixelMeta = Pixels.GetMeta();
if ( mParams.mLiveUseClockTime )
Packet.mTimecode = SoyTime(true);
else
Packet.mTimecode = GetSeekTime();
Packet.mMeta.mTransform = Transform;
Packet.mData.Copy( Pixels.GetPixelsArray() );
mWindowLastPos = WindowPos;
OnPacketExtracted( Packet.mTimecode, Packet.mMeta.mStreamIndex );
return pPacket;
}
void HwndExtractor::GetMeta(TJsonWriter& Json)
{
TMediaExtractor::GetMeta( Json );
Json.Push("WindowPosition", mWindowLastPos );
}
#pragma once
#include <SoyMedia.h>
namespace Hwnd
{
void EnumWindows(std::function<void(const std::string&)> AppendName,std::function<bool()> Block);
}
#define INVALID_HWND 0
class TWindowHandle
{
public:
TWindowHandle() :
mHandle ( INVALID_HWND )
{
}
bool IsValid() const { return mHandle != INVALID_HWND; }
public:
RECT mRect;
HWND mHandle;
std::string mName;
};
class HwndExtractor : public TMediaExtractor
{
public:
HwndExtractor(const TMediaExtractorParams& Params);
virtual void GetStreams(ArrayBridge<TStreamMeta>&& Streams) override;
virtual std::shared_ptr<Platform::TMediaFormat> GetStreamFormat(size_t StreamIndex) override { return nullptr; }
virtual void GetMeta(TJsonWriter& Json) override;
protected:
virtual std::shared_ptr<TMediaPacket> ReadNextPacket() override;
TWindowHandle GetWindowHandle();
public:
std::string mWindowTitle;
vec2x<int> mWindowLastPos; // cache for meta
TWindowHandle mWindowHandle;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment