2012-09-18 01:05:42 +04:00
/**
2012-10-09 07:02:04 +04:00
* FreeRDP : A Remote Desktop Protocol Implementation
2012-09-18 01:05:42 +04:00
* FreeRDP Windows Server
*
* Copyright 2012 Corey Clayton < can . of . tuna @ gmail . com >
*
* Licensed under the Apache License , Version 2.0 ( the " License " ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an " AS IS " BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
*/
2012-09-20 02:16:49 +04:00
2012-09-06 04:03:07 +04:00
# ifdef HAVE_CONFIG_H
# include "config.h"
# endif
2012-09-18 01:05:42 +04:00
2012-09-19 03:00:03 +04:00
# include "wf_interface.h"
2014-07-18 02:27:40 +04:00
# ifdef WITH_DXGI_1_2
2012-09-19 03:00:03 +04:00
2012-09-18 01:05:42 +04:00
# define CINTERFACE
# include <D3D11.h>
# include <dxgi1_2.h>
# include <tchar.h>
# include "wf_dxgi.h"
2012-09-20 02:16:49 +04:00
/* Driver types supported */
2012-09-18 01:05:42 +04:00
D3D_DRIVER_TYPE DriverTypes [ ] =
{
D3D_DRIVER_TYPE_HARDWARE ,
D3D_DRIVER_TYPE_WARP ,
D3D_DRIVER_TYPE_REFERENCE ,
} ;
UINT NumDriverTypes = ARRAYSIZE ( DriverTypes ) ;
2012-09-20 02:16:49 +04:00
/* Feature levels supported */
2012-09-18 01:05:42 +04:00
D3D_FEATURE_LEVEL FeatureLevels [ ] =
{
D3D_FEATURE_LEVEL_11_0 ,
D3D_FEATURE_LEVEL_10_1 ,
D3D_FEATURE_LEVEL_10_0 ,
D3D_FEATURE_LEVEL_9_1
} ;
2012-09-20 02:16:49 +04:00
2012-09-18 01:05:42 +04:00
UINT NumFeatureLevels = ARRAYSIZE ( FeatureLevels ) ;
D3D_FEATURE_LEVEL FeatureLevel ;
2012-09-20 02:16:49 +04:00
ID3D11Device * gDevice = NULL ;
ID3D11DeviceContext * gContext = NULL ;
IDXGIOutputDuplication * gOutputDuplication = NULL ;
ID3D11Texture2D * gAcquiredDesktopImage = NULL ;
2012-09-18 01:05:42 +04:00
IDXGISurface * surf ;
2012-09-20 02:16:49 +04:00
ID3D11Texture2D * sStage ;
2012-09-18 01:05:42 +04:00
DXGI_OUTDUPL_FRAME_INFO FrameInfo ;
2012-11-01 06:10:27 +04:00
int wf_dxgi_init ( wfInfo * wfi )
2012-10-18 00:17:19 +04:00
{
gAcquiredDesktopImage = NULL ;
2012-11-01 06:10:27 +04:00
if ( wf_dxgi_createDevice ( wfi ) ! = 0 )
2012-10-18 00:17:19 +04:00
{
return 1 ;
}
2012-11-01 06:10:27 +04:00
if ( wf_dxgi_getDuplication ( wfi ) ! = 0 )
2012-10-18 00:17:19 +04:00
{
return 1 ;
}
return 0 ;
}
2012-11-01 06:10:27 +04:00
int wf_dxgi_createDevice ( wfInfo * wfi )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
HRESULT status ;
2012-09-18 01:05:42 +04:00
UINT DriverTypeIndex ;
for ( DriverTypeIndex = 0 ; DriverTypeIndex < NumDriverTypes ; + + DriverTypeIndex )
{
2012-10-23 02:43:37 +04:00
status = D3D11CreateDevice ( NULL , DriverTypes [ DriverTypeIndex ] , NULL , 0 , FeatureLevels , NumFeatureLevels ,
2012-09-20 02:16:49 +04:00
D3D11_SDK_VERSION , & gDevice , & FeatureLevel , & gContext ) ;
if ( SUCCEEDED ( status ) )
2012-09-18 01:05:42 +04:00
break ;
2012-10-17 08:43:59 +04:00
_tprintf ( _T ( " D3D11CreateDevice returned [%d] for Driver Type %d \n " ) , status , DriverTypes [ DriverTypeIndex ] ) ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to create device in InitializeDx \n " ) ) ;
2012-10-17 10:29:51 +04:00
return 1 ;
2012-09-18 01:05:42 +04:00
}
2012-10-18 00:17:19 +04:00
return 0 ;
}
2012-11-01 06:10:27 +04:00
int wf_dxgi_getDuplication ( wfInfo * wfi )
2012-10-18 00:17:19 +04:00
{
HRESULT status ;
UINT dTop , i = 0 ;
DXGI_OUTPUT_DESC desc ;
IDXGIOutput * pOutput ;
IDXGIDevice * DxgiDevice = NULL ;
IDXGIAdapter * DxgiAdapter = NULL ;
IDXGIOutput * DxgiOutput = NULL ;
IDXGIOutput1 * DxgiOutput1 = NULL ;
2012-09-20 02:16:49 +04:00
status = gDevice - > lpVtbl - > QueryInterface ( gDevice , & IID_IDXGIDevice , ( void * * ) & DxgiDevice ) ;
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
if ( FAILED ( status ) )
{
_tprintf ( _T ( " Failed to get QI for DXGI Device \n " ) ) ;
return 1 ;
}
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
status = DxgiDevice - > lpVtbl - > GetParent ( DxgiDevice , & IID_IDXGIAdapter , ( void * * ) & DxgiAdapter ) ;
2012-09-18 01:05:42 +04:00
DxgiDevice - > lpVtbl - > Release ( DxgiDevice ) ;
2012-09-20 02:16:49 +04:00
DxgiDevice = NULL ;
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
if ( FAILED ( status ) )
{
_tprintf ( _T ( " Failed to get parent DXGI Adapter \n " ) ) ;
return 1 ;
}
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
ZeroMemory ( & desc , sizeof ( desc ) ) ;
2012-09-18 01:05:42 +04:00
pOutput = NULL ;
2012-09-20 02:16:49 +04:00
while ( DxgiAdapter - > lpVtbl - > EnumOutputs ( DxgiAdapter , i , & pOutput ) ! = DXGI_ERROR_NOT_FOUND )
2012-09-18 01:05:42 +04:00
{
DXGI_OUTPUT_DESC * pDesc = & desc ;
2012-09-20 02:16:49 +04:00
status = pOutput - > lpVtbl - > GetDesc ( pOutput , pDesc ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to get description \n " ) ) ;
return 1 ;
}
_tprintf ( _T ( " Output %d: [%s] [%d] \n " ) , i , pDesc - > DeviceName , pDesc - > AttachedToDesktop ) ;
2012-09-20 02:16:49 +04:00
if ( pDesc - > AttachedToDesktop )
2012-09-18 01:05:42 +04:00
dTop = i ;
pOutput - > lpVtbl - > Release ( pOutput ) ;
+ + i ;
}
2012-11-01 06:10:27 +04:00
dTop = wfi - > screenID ;
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
status = DxgiAdapter - > lpVtbl - > EnumOutputs ( DxgiAdapter , dTop , & DxgiOutput ) ;
DxgiAdapter - > lpVtbl - > Release ( DxgiAdapter ) ;
DxgiAdapter = NULL ;
if ( FAILED ( status ) )
{
_tprintf ( _T ( " Failed to get output \n " ) ) ;
2012-09-18 01:05:42 +04:00
return 1 ;
}
2012-09-20 02:16:49 +04:00
status = DxgiOutput - > lpVtbl - > QueryInterface ( DxgiOutput , & IID_IDXGIOutput1 , ( void * * ) & DxgiOutput1 ) ;
2012-09-18 01:05:42 +04:00
DxgiOutput - > lpVtbl - > Release ( DxgiOutput ) ;
2012-09-20 02:16:49 +04:00
DxgiOutput = NULL ;
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
if ( FAILED ( status ) )
{
_tprintf ( _T ( " Failed to get IDXGIOutput1 \n " ) ) ;
return 1 ;
}
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
status = DxgiOutput1 - > lpVtbl - > DuplicateOutput ( DxgiOutput1 , ( IUnknown * ) gDevice , & gOutputDuplication ) ;
2012-09-18 01:05:42 +04:00
DxgiOutput1 - > lpVtbl - > Release ( DxgiOutput1 ) ;
2012-09-20 02:16:49 +04:00
DxgiOutput1 = NULL ;
if ( FAILED ( status ) )
{
if ( status = = DXGI_ERROR_NOT_CURRENTLY_AVAILABLE )
{
_tprintf ( _T ( " There is already the maximum number of applications using the Desktop Duplication API running, please close one of those applications and then try again. \n " ) ) ;
2012-09-18 01:05:42 +04:00
return 1 ;
2012-09-20 02:16:49 +04:00
}
2012-10-18 00:50:39 +04:00
_tprintf ( _T ( " Failed to get duplicate output. Status = %#X \n " ) , status ) ;
2012-09-18 01:05:42 +04:00
return 1 ;
2012-09-20 02:16:49 +04:00
}
2012-09-18 22:07:38 +04:00
return 0 ;
2012-09-18 01:05:42 +04:00
}
2012-10-18 00:17:19 +04:00
2012-09-19 21:42:22 +04:00
int wf_dxgi_cleanup ( wfInfo * wfi )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
if ( wfi - > framesWaiting > 0 )
2012-09-19 21:42:22 +04:00
{
wf_dxgi_releasePixelData ( wfi ) ;
}
2012-09-20 02:16:49 +04:00
if ( gAcquiredDesktopImage )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
gAcquiredDesktopImage - > lpVtbl - > Release ( gAcquiredDesktopImage ) ;
gAcquiredDesktopImage = NULL ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
if ( gOutputDuplication )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
gOutputDuplication - > lpVtbl - > Release ( gOutputDuplication ) ;
gOutputDuplication = NULL ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
if ( gContext )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
gContext - > lpVtbl - > Release ( gContext ) ;
gContext = NULL ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
if ( gDevice )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
gDevice - > lpVtbl - > Release ( gDevice ) ;
gDevice = NULL ;
2012-09-18 01:05:42 +04:00
}
return 0 ;
}
2012-09-18 22:07:38 +04:00
int wf_dxgi_nextFrame ( wfInfo * wfi , UINT timeout )
2012-09-18 01:05:42 +04:00
{
2012-10-17 23:31:36 +04:00
HRESULT status = 0 ;
2012-09-18 22:07:38 +04:00
UINT i = 0 ;
2012-09-20 02:16:49 +04:00
UINT DataBufferSize = 0 ;
BYTE * DataBuffer = NULL ;
IDXGIResource * DesktopResource = NULL ;
2012-09-18 22:07:38 +04:00
2012-09-20 02:16:49 +04:00
if ( wfi - > framesWaiting > 0 )
2012-09-18 22:07:38 +04:00
{
wf_dxgi_releasePixelData ( wfi ) ;
}
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
if ( gAcquiredDesktopImage )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
gAcquiredDesktopImage - > lpVtbl - > Release ( gAcquiredDesktopImage ) ;
gAcquiredDesktopImage = NULL ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
status = gOutputDuplication - > lpVtbl - > AcquireNextFrame ( gOutputDuplication , timeout , & FrameInfo , & DesktopResource ) ;
if ( status = = DXGI_ERROR_WAIT_TIMEOUT )
2012-09-18 01:05:42 +04:00
{
return 1 ;
}
2012-09-20 02:16:49 +04:00
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
2012-10-18 00:17:19 +04:00
if ( status = = DXGI_ERROR_ACCESS_LOST )
2012-09-18 01:05:42 +04:00
{
2012-10-18 00:17:19 +04:00
_tprintf ( _T ( " Failed to acquire next frame with status=%#X \n " ) , status ) ;
_tprintf ( _T ( " Trying to reinitialize due to ACCESS LOST... " ) ) ;
2012-10-18 00:50:39 +04:00
if ( gAcquiredDesktopImage )
{
gAcquiredDesktopImage - > lpVtbl - > Release ( gAcquiredDesktopImage ) ;
gAcquiredDesktopImage = NULL ;
}
if ( gOutputDuplication )
{
gOutputDuplication - > lpVtbl - > Release ( gOutputDuplication ) ;
gOutputDuplication = NULL ;
}
2012-10-18 00:17:19 +04:00
wf_dxgi_getDuplication ( wfi ) ;
2012-10-18 00:50:39 +04:00
return 1 ;
2012-09-18 01:05:42 +04:00
}
2012-10-18 00:17:19 +04:00
else
{
_tprintf ( _T ( " Failed to acquire next frame with status=%#X \n " ) , status ) ;
status = gOutputDuplication - > lpVtbl - > ReleaseFrame ( gOutputDuplication ) ;
if ( FAILED ( status ) )
{
2012-10-18 01:06:03 +04:00
_tprintf ( _T ( " Failed to release frame with status=%d \n " ) , status ) ;
2012-10-18 00:17:19 +04:00
}
2012-09-20 02:16:49 +04:00
2012-10-18 00:17:19 +04:00
return 1 ;
}
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
status = DesktopResource - > lpVtbl - > QueryInterface ( DesktopResource , & IID_ID3D11Texture2D , ( void * * ) & gAcquiredDesktopImage ) ;
2012-09-18 01:05:42 +04:00
DesktopResource - > lpVtbl - > Release ( DesktopResource ) ;
DesktopResource = NULL ;
2012-09-20 02:16:49 +04:00
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
return 1 ;
}
2012-09-18 22:07:38 +04:00
wfi - > framesWaiting = FrameInfo . AccumulatedFrames ;
2012-09-18 01:05:42 +04:00
2012-10-18 02:39:04 +04:00
if ( FrameInfo . AccumulatedFrames = = 0 )
{
status = gOutputDuplication - > lpVtbl - > ReleaseFrame ( gOutputDuplication ) ;
if ( FAILED ( status ) )
{
_tprintf ( _T ( " Failed to release frame with status=%d \n " ) , status ) ;
}
}
2012-09-18 01:05:42 +04:00
return 0 ;
}
2012-11-01 06:10:27 +04:00
int wf_dxgi_getPixelData ( wfInfo * wfi , BYTE * * data , int * pitch , RECT * invalid )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
HRESULT status ;
2012-09-18 01:05:42 +04:00
D3D11_BOX Box ;
2012-09-20 02:16:49 +04:00
DXGI_MAPPED_RECT mappedRect ;
D3D11_TEXTURE2D_DESC tDesc ;
2012-09-18 01:05:42 +04:00
2012-09-19 01:25:20 +04:00
tDesc . Width = ( invalid - > right - invalid - > left ) ;
tDesc . Height = ( invalid - > bottom - invalid - > top ) ;
2012-09-18 01:05:42 +04:00
tDesc . MipLevels = 1 ;
tDesc . ArraySize = 1 ;
tDesc . Format = DXGI_FORMAT_B8G8R8A8_UNORM ;
tDesc . SampleDesc . Count = 1 ;
tDesc . SampleDesc . Quality = 0 ;
tDesc . Usage = D3D11_USAGE_STAGING ;
tDesc . BindFlags = 0 ;
2012-09-20 02:16:49 +04:00
tDesc . CPUAccessFlags = D3D11_CPU_ACCESS_READ ;
2012-09-18 01:05:42 +04:00
tDesc . MiscFlags = 0 ;
Box . top = invalid - > top ;
Box . left = invalid - > left ;
Box . right = invalid - > right ;
Box . bottom = invalid - > bottom ;
Box . front = 0 ;
Box . back = 1 ;
2012-09-20 02:16:49 +04:00
status = gDevice - > lpVtbl - > CreateTexture2D ( gDevice , & tDesc , NULL , & sStage ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to create staging surface \n " ) ) ;
exit ( 1 ) ;
2012-09-18 22:07:38 +04:00
return 1 ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
gContext - > lpVtbl - > CopySubresourceRegion ( gContext , ( ID3D11Resource * ) sStage , 0 , 0 , 0 , 0 , ( ID3D11Resource * ) gAcquiredDesktopImage , 0 , & Box ) ;
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
status = sStage - > lpVtbl - > QueryInterface ( sStage , & IID_IDXGISurface , ( void * * ) & surf ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to QI staging surface \n " ) ) ;
exit ( 1 ) ;
2012-09-18 22:07:38 +04:00
return 1 ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
surf - > lpVtbl - > Map ( surf , & mappedRect , DXGI_MAP_READ ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to map staging surface \n " ) ) ;
exit ( 1 ) ;
2012-09-18 22:07:38 +04:00
return 1 ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
* data = mappedRect . pBits ;
* pitch = mappedRect . Pitch ;
2012-09-18 01:05:42 +04:00
2012-09-18 22:07:38 +04:00
return 0 ;
2012-09-18 01:05:42 +04:00
}
2012-09-18 22:07:38 +04:00
int wf_dxgi_releasePixelData ( wfInfo * wfi )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
HRESULT status ;
2012-09-18 01:05:42 +04:00
surf - > lpVtbl - > Unmap ( surf ) ;
surf - > lpVtbl - > Release ( surf ) ;
surf = NULL ;
sStage - > lpVtbl - > Release ( sStage ) ;
sStage = NULL ;
2012-09-20 02:16:49 +04:00
status = gOutputDuplication - > lpVtbl - > ReleaseFrame ( gOutputDuplication ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to release frame \n " ) ) ;
return 1 ;
}
2012-09-18 22:07:38 +04:00
wfi - > framesWaiting = 0 ;
2012-09-20 02:16:49 +04:00
2012-09-18 22:07:38 +04:00
return 0 ;
2012-09-18 01:05:42 +04:00
}
int wf_dxgi_getInvalidRegion ( RECT * invalid )
{
UINT i ;
2012-09-20 02:16:49 +04:00
HRESULT status ;
2012-09-18 01:05:42 +04:00
UINT dirty ;
2012-09-20 02:16:49 +04:00
UINT BufSize ;
2012-09-18 01:05:42 +04:00
RECT * pRect ;
2012-09-20 02:16:49 +04:00
BYTE * DirtyRects ;
UINT DataBufferSize = 0 ;
BYTE * DataBuffer = NULL ;
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
if ( FrameInfo . AccumulatedFrames = = 0 )
2012-09-18 22:07:38 +04:00
{
return 1 ;
}
2012-09-18 01:05:42 +04:00
2012-09-20 02:16:49 +04:00
if ( FrameInfo . TotalMetadataBufferSize )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
if ( FrameInfo . TotalMetadataBufferSize > DataBufferSize )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
if ( DataBuffer )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
free ( DataBuffer ) ;
DataBuffer = NULL ;
2012-09-18 01:05:42 +04:00
}
2012-09-20 02:16:49 +04:00
DataBuffer = ( BYTE * ) malloc ( FrameInfo . TotalMetadataBufferSize ) ;
if ( ! DataBuffer )
2012-09-18 01:05:42 +04:00
{
2012-09-20 02:16:49 +04:00
DataBufferSize = 0 ;
2012-09-18 01:05:42 +04:00
_tprintf ( _T ( " Failed to allocate memory for metadata \n " ) ) ;
exit ( 1 ) ;
}
2012-09-20 02:16:49 +04:00
DataBufferSize = FrameInfo . TotalMetadataBufferSize ;
2012-09-18 01:05:42 +04:00
}
BufSize = FrameInfo . TotalMetadataBufferSize ;
2012-09-20 02:16:49 +04:00
status = gOutputDuplication - > lpVtbl - > GetFrameMoveRects ( gOutputDuplication , BufSize , ( DXGI_OUTDUPL_MOVE_RECT * ) DataBuffer , & BufSize ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to get frame move rects \n " ) ) ;
return 1 ;
}
2012-09-20 02:16:49 +04:00
DirtyRects = DataBuffer + BufSize ;
2012-09-18 01:05:42 +04:00
BufSize = FrameInfo . TotalMetadataBufferSize - BufSize ;
2012-09-20 02:16:49 +04:00
status = gOutputDuplication - > lpVtbl - > GetFrameDirtyRects ( gOutputDuplication , BufSize , ( RECT * ) DirtyRects , & BufSize ) ;
if ( FAILED ( status ) )
2012-09-18 01:05:42 +04:00
{
_tprintf ( _T ( " Failed to get frame dirty rects \n " ) ) ;
return 1 ;
}
dirty = BufSize / sizeof ( RECT ) ;
pRect = ( RECT * ) DirtyRects ;
2012-09-20 02:16:49 +04:00
2012-09-18 01:05:42 +04:00
for ( i = 0 ; i < dirty ; + + i )
{
UnionRect ( invalid , invalid , pRect ) ;
+ + pRect ;
}
}
return 0 ;
2012-09-18 01:57:21 +04:00
}
2012-09-06 04:03:07 +04:00
2012-09-19 03:00:03 +04:00
# endif