Created
June 20, 2016 14:15
-
-
Save SoylentGraham/fd9b434c35cbeb43d55add2b81afb542 to your computer and use it in GitHub Desktop.
Windows Window capture in PopMovie.xyz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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