2018-06-28 08:44:43 +00:00
# include "session.hpp"
# include "settings/streamingpreferences.h"
# include <Limelight.h>
# include <SDL.h>
2018-07-08 04:52:20 +00:00
# include "utils.h"
2018-06-28 08:44:43 +00:00
2018-08-21 04:36:23 +00:00
// HACK: Need to call SetThreadExecutionState() because SDL doesn't
# ifdef Q_OS_WIN32
# define WIN32_LEAN_AND_MEAN
# include <Windows.h>
# endif
2018-07-22 00:32:00 +00:00
# ifdef HAVE_FFMPEG
2018-07-18 03:00:16 +00:00
# include "video/ffmpeg.h"
2018-07-22 00:32:00 +00:00
# endif
2018-07-18 03:00:16 +00:00
2018-07-22 03:22:00 +00:00
# ifdef HAVE_SLVIDEO
# include "video/sl.h"
# endif
2018-07-23 00:42:31 +00:00
# ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
2018-07-23 00:07:45 +00:00
# define ICON_SIZE 32
2018-07-23 00:42:31 +00:00
# else
# define ICON_SIZE 64
# endif
2018-07-23 00:07:45 +00:00
2018-07-22 01:47:41 +00:00
# include <openssl/rand.h>
2018-06-28 08:44:43 +00:00
# include <QtEndian>
# include <QCoreApplication>
2018-07-09 07:09:06 +00:00
# include <QThreadPool>
2018-07-23 00:07:45 +00:00
# include <QSvgRenderer>
# include <QPainter>
# include <QImage>
2018-06-28 08:44:43 +00:00
CONNECTION_LISTENER_CALLBACKS Session : : k_ConnCallbacks = {
Session : : clStageStarting ,
nullptr ,
Session : : clStageFailed ,
nullptr ,
Session : : clConnectionTerminated ,
nullptr ,
nullptr ,
Session : : clLogMessage
} ;
AUDIO_RENDERER_CALLBACKS Session : : k_AudioCallbacks = {
Session : : sdlAudioInit ,
Session : : sdlAudioStart ,
Session : : sdlAudioStop ,
Session : : sdlAudioCleanup ,
Session : : sdlAudioDecodeAndPlaySample ,
CAPABILITY_DIRECT_SUBMIT
} ;
Session * Session : : s_ActiveSession ;
2018-07-09 07:09:06 +00:00
QSemaphore Session : : s_ActiveSessionSemaphore ( 1 ) ;
2018-06-28 08:44:43 +00:00
void Session : : clStageStarting ( int stage )
{
// We know this is called on the same thread as LiStartConnection()
// which happens to be the main thread, so it's cool to interact
// with the GUI in these callbacks.
2018-07-07 23:30:26 +00:00
emit s_ActiveSession - > stageStarting ( QString : : fromLocal8Bit ( LiGetStageName ( stage ) ) ) ;
2018-06-28 08:44:43 +00:00
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
void Session : : clStageFailed ( int stage , long errorCode )
{
// We know this is called on the same thread as LiStartConnection()
// which happens to be the main thread, so it's cool to interact
// with the GUI in these callbacks.
2018-07-07 23:30:26 +00:00
emit s_ActiveSession - > stageFailed ( QString : : fromLocal8Bit ( LiGetStageName ( stage ) ) , errorCode ) ;
2018-06-28 08:44:43 +00:00
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
void Session : : clConnectionTerminated ( long errorCode )
{
2018-09-02 22:34:10 +00:00
emit s_ActiveSession - > displayLaunchError ( " Connection terminated " ) ;
2018-06-28 08:44:43 +00:00
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Connection terminated: %ld " ,
errorCode ) ;
// Push a quit event to the main loop
SDL_Event event ;
event . type = SDL_QUIT ;
event . quit . timestamp = SDL_GetTicks ( ) ;
SDL_PushEvent ( & event ) ;
}
void Session : : clLogMessage ( const char * format , . . . )
{
va_list ap ;
va_start ( ap , format ) ;
SDL_LogMessageV ( SDL_LOG_CATEGORY_APPLICATION ,
SDL_LOG_PRIORITY_INFO ,
format ,
ap ) ;
va_end ( ap ) ;
}
2018-08-31 14:40:25 +00:00
# define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
2018-07-18 03:00:16 +00:00
bool Session : : chooseDecoder ( StreamingPreferences : : VideoDecoderSelection vds ,
SDL_Window * window , int videoFormat , int width , int height ,
2018-08-21 01:19:42 +00:00
int frameRate , bool enableVsync , IVideoDecoder * & chosenDecoder )
2018-07-18 03:00:16 +00:00
{
2018-07-22 03:22:00 +00:00
# ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder ( ) ;
2018-08-31 14:40:25 +00:00
if ( CALL_INITIALIZE ( chosenDecoder ) ) {
2018-07-22 03:22:00 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" SLVideo video decoder chosen " ) ;
return true ;
}
else {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Unable to load SLVideo decoder " ) ;
delete chosenDecoder ;
chosenDecoder = nullptr ;
}
# endif
2018-07-22 00:00:09 +00:00
# ifdef HAVE_FFMPEG
2018-07-18 03:00:16 +00:00
chosenDecoder = new FFmpegVideoDecoder ( ) ;
2018-08-31 14:40:25 +00:00
if ( CALL_INITIALIZE ( chosenDecoder ) ) {
2018-07-18 03:00:16 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" FFmpeg-based video decoder chosen " ) ;
return true ;
}
else {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Unable to load FFmpeg decoder " ) ;
delete chosenDecoder ;
chosenDecoder = nullptr ;
}
2018-07-22 00:00:09 +00:00
# endif
2018-07-18 03:00:16 +00:00
2018-08-03 09:11:44 +00:00
# if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO)
# error No video decoding libraries available!
# endif
2018-07-18 03:00:16 +00:00
// If we reach this, we didn't initialize any decoders successfully
return false ;
}
int Session : : drSetup ( int videoFormat , int width , int height , int frameRate , void * , int )
{
2018-07-20 22:31:57 +00:00
s_ActiveSession - > m_ActiveVideoFormat = videoFormat ;
s_ActiveSession - > m_ActiveVideoWidth = width ;
s_ActiveSession - > m_ActiveVideoHeight = height ;
s_ActiveSession - > m_ActiveVideoFrameRate = frameRate ;
2018-07-21 02:55:07 +00:00
// Defer decoder setup until we've started streaming so we
// don't have to hide and show the SDL window (which seems to
// cause pointer hiding to break on Windows).
2018-07-18 03:00:16 +00:00
2018-07-28 23:06:26 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION , " Video stream is %dx%dx%d (format 0x%x) " ,
width , height , frameRate , videoFormat ) ;
2018-07-18 03:00:16 +00:00
return 0 ;
}
int Session : : drSubmitDecodeUnit ( PDECODE_UNIT du )
{
// Use a lock since we'll be yanking this decoder out
// from underneath the session when we initiate destruction.
// We need to destroy the decoder on the main thread to satisfy
// some API constraints (like DXVA2).
2018-07-20 22:31:57 +00:00
2018-07-18 03:00:16 +00:00
SDL_AtomicLock ( & s_ActiveSession - > m_DecoderLock ) ;
2018-07-20 22:31:57 +00:00
if ( s_ActiveSession - > m_NeedsIdr ) {
// If we reset our decoder, we'll need to request an IDR frame
s_ActiveSession - > m_NeedsIdr = false ;
SDL_AtomicUnlock ( & s_ActiveSession - > m_DecoderLock ) ;
return DR_NEED_IDR ;
}
2018-07-18 03:00:16 +00:00
IVideoDecoder * decoder = s_ActiveSession - > m_VideoDecoder ;
if ( decoder ! = nullptr ) {
int ret = decoder - > submitDecodeUnit ( du ) ;
SDL_AtomicUnlock ( & s_ActiveSession - > m_DecoderLock ) ;
return ret ;
}
else {
SDL_AtomicUnlock ( & s_ActiveSession - > m_DecoderLock ) ;
return DR_OK ;
}
}
bool Session : : isHardwareDecodeAvailable ( StreamingPreferences : : VideoDecoderSelection vds ,
int videoFormat , int width , int height , int frameRate )
{
IVideoDecoder * decoder ;
2018-07-21 01:15:46 +00:00
if ( SDL_InitSubSystem ( SDL_INIT_VIDEO ) ! = 0 ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s " ,
SDL_GetError ( ) ) ;
return false ;
}
2018-07-18 03:00:16 +00:00
SDL_Window * window = SDL_CreateWindow ( " " , 0 , 0 , width , height , SDL_WINDOW_HIDDEN ) ;
if ( ! window ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Failed to create window for hardware decode test: %s " ,
SDL_GetError ( ) ) ;
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-07-18 03:00:16 +00:00
return false ;
}
2018-08-21 01:19:42 +00:00
if ( ! chooseDecoder ( vds , window , videoFormat , width , height , frameRate , true , decoder ) ) {
2018-07-18 03:00:16 +00:00
SDL_DestroyWindow ( window ) ;
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-07-18 03:00:16 +00:00
return false ;
}
SDL_DestroyWindow ( window ) ;
bool ret = decoder - > isHardwareAccelerated ( ) ;
2018-07-21 01:15:46 +00:00
2018-07-18 03:00:16 +00:00
delete decoder ;
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-07-18 03:00:16 +00:00
return ret ;
}
2018-08-25 20:36:54 +00:00
int Session : : getDecoderCapabilities ( StreamingPreferences : : VideoDecoderSelection vds ,
int videoFormat , int width , int height , int frameRate )
{
IVideoDecoder * decoder ;
if ( SDL_InitSubSystem ( SDL_INIT_VIDEO ) ! = 0 ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s " ,
SDL_GetError ( ) ) ;
return false ;
}
SDL_Window * window = SDL_CreateWindow ( " " , 0 , 0 , width , height , SDL_WINDOW_HIDDEN ) ;
if ( ! window ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Failed to create window for hardware decode test: %s " ,
SDL_GetError ( ) ) ;
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
return false ;
}
if ( ! chooseDecoder ( vds , window , videoFormat , width , height , frameRate , true , decoder ) ) {
SDL_DestroyWindow ( window ) ;
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
return false ;
}
SDL_DestroyWindow ( window ) ;
int caps = decoder - > getDecoderCapabilities ( ) ;
delete decoder ;
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
return caps ;
}
2018-06-28 08:44:43 +00:00
Session : : Session ( NvComputer * computer , NvApp & app )
: m_Computer ( computer ) ,
2018-07-09 07:09:06 +00:00
m_App ( app ) ,
2018-07-18 03:00:16 +00:00
m_Window ( nullptr ) ,
m_VideoDecoder ( nullptr ) ,
2018-07-20 22:31:57 +00:00
m_DecoderLock ( 0 ) ,
2018-08-31 04:09:31 +00:00
m_NeedsIdr ( false ) ,
m_AudioDisabled ( false )
2018-06-28 08:44:43 +00:00
{
2018-08-05 20:32:04 +00:00
qDebug ( ) < < " Server GPU: " < < m_Computer - > gpuModel ;
2018-07-08 04:52:20 +00:00
LiInitializeVideoCallbacks ( & m_VideoCallbacks ) ;
m_VideoCallbacks . setup = drSetup ;
m_VideoCallbacks . submitDecodeUnit = drSubmitDecodeUnit ;
2018-07-18 03:00:16 +00:00
// Submit for decode without using a separate thread
m_VideoCallbacks . capabilities | = CAPABILITY_DIRECT_SUBMIT ;
// Slice up to 4 times for parallel decode, once slice per core
2018-08-05 20:32:04 +00:00
int slices = qMin ( MAX_SLICES , SDL_GetCPUCount ( ) ) ;
m_VideoCallbacks . capabilities | = CAPABILITY_SLICES_PER_FRAME ( slices ) ;
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Encoder configured for %d slices per frame " ,
slices ) ;
2018-06-28 08:44:43 +00:00
LiInitializeStreamConfiguration ( & m_StreamConfig ) ;
2018-07-08 04:52:20 +00:00
m_StreamConfig . width = m_Preferences . width ;
m_StreamConfig . height = m_Preferences . height ;
m_StreamConfig . fps = m_Preferences . fps ;
m_StreamConfig . bitrate = m_Preferences . bitrateKbps ;
2018-06-28 08:44:43 +00:00
m_StreamConfig . hevcBitratePercentageMultiplier = 75 ;
2018-08-05 20:32:04 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Video bitrate: %d kbps " ,
m_StreamConfig . bitrate ) ;
2018-07-22 01:47:41 +00:00
RAND_bytes ( reinterpret_cast < unsigned char * > ( m_StreamConfig . remoteInputAesKey ) ,
sizeof ( m_StreamConfig . remoteInputAesKey ) ) ;
// Only the first 4 bytes are populated in the RI key IV
RAND_bytes ( reinterpret_cast < unsigned char * > ( m_StreamConfig . remoteInputAesIv ) , 4 ) ;
2018-07-08 04:52:20 +00:00
switch ( m_Preferences . audioConfig )
2018-06-28 08:44:43 +00:00
{
case StreamingPreferences : : AC_AUTO :
2018-07-28 23:06:26 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION , " Autodetecting audio configuration " ) ;
2018-06-28 08:44:43 +00:00
m_StreamConfig . audioConfiguration = sdlDetermineAudioConfiguration ( ) ;
break ;
case StreamingPreferences : : AC_FORCE_STEREO :
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_STEREO ;
break ;
case StreamingPreferences : : AC_FORCE_SURROUND :
m_StreamConfig . audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND ;
break ;
}
2018-07-13 09:28:10 +00:00
2018-07-28 23:06:26 +00:00
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Audio configuration: %d " ,
m_StreamConfig . audioConfiguration ) ;
2018-07-08 04:52:20 +00:00
switch ( m_Preferences . videoCodecConfig )
2018-06-28 08:44:43 +00:00
{
case StreamingPreferences : : VCC_AUTO :
// TODO: Determine if HEVC is better depending on the decoder
2018-07-13 09:28:10 +00:00
m_StreamConfig . supportsHevc =
isHardwareDecodeAvailable ( m_Preferences . videoDecoderSelection ,
VIDEO_FORMAT_H265 ,
m_StreamConfig . width ,
2018-07-18 03:00:16 +00:00
m_StreamConfig . height ,
m_StreamConfig . fps ) ;
2018-06-28 08:44:43 +00:00
m_StreamConfig . enableHdr = false ;
break ;
case StreamingPreferences : : VCC_FORCE_H264 :
m_StreamConfig . supportsHevc = false ;
m_StreamConfig . enableHdr = false ;
break ;
case StreamingPreferences : : VCC_FORCE_HEVC :
m_StreamConfig . supportsHevc = true ;
m_StreamConfig . enableHdr = false ;
break ;
case StreamingPreferences : : VCC_FORCE_HEVC_HDR :
m_StreamConfig . supportsHevc = true ;
m_StreamConfig . enableHdr = true ;
break ;
}
2018-07-16 08:12:53 +00:00
if ( computer - > activeAddress = = computer - > remoteAddress ) {
m_StreamConfig . streamingRemotely = 1 ;
}
else {
m_StreamConfig . streamingRemotely = 0 ;
}
if ( computer - > activeAddress = = computer - > localAddress ) {
m_StreamConfig . packetSize = 1392 ;
}
else {
m_StreamConfig . packetSize = 1024 ;
}
2018-09-04 02:17:34 +00:00
switch ( m_Preferences . windowMode )
{
case StreamingPreferences : : WM_FULLSCREEN_DESKTOP :
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP ;
break ;
case StreamingPreferences : : WM_FULLSCREEN :
default :
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN ;
break ;
}
2018-06-28 08:44:43 +00:00
}
2018-08-04 23:05:37 +00:00
void Session : : emitLaunchWarning ( QString text )
{
// Emit the warning to the UI
emit displayLaunchWarning ( text ) ;
// Wait a little bit so the user can actually read what we just said.
// This wait is a little longer than the actual toast timeout (3 seconds)
// to allow it to transition off the screen before continuing.
uint32_t start = SDL_GetTicks ( ) ;
while ( ! SDL_TICKS_PASSED ( SDL_GetTicks ( ) , start + 3500 ) ) {
// Pump the UI loop while we wait
SDL_Delay ( 5 ) ;
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
}
}
2018-07-07 23:30:26 +00:00
bool Session : : validateLaunch ( )
2018-06-28 08:44:43 +00:00
{
QStringList warningList ;
2018-08-04 23:15:13 +00:00
if ( m_Preferences . videoDecoderSelection = = StreamingPreferences : : VDS_FORCE_SOFTWARE ) {
2018-08-04 23:45:31 +00:00
emitLaunchWarning ( " Your settings selection to force software decoding may cause poor streaming performance. " ) ;
2018-08-04 23:15:13 +00:00
}
2018-07-09 03:53:24 +00:00
if ( m_StreamConfig . supportsHevc ) {
2018-07-27 02:26:22 +00:00
bool hevcForced = m_Preferences . videoCodecConfig = = StreamingPreferences : : VCC_FORCE_HEVC | |
m_Preferences . videoCodecConfig = = StreamingPreferences : : VCC_FORCE_HEVC_HDR ;
if ( ! isHardwareDecodeAvailable ( m_Preferences . videoDecoderSelection ,
VIDEO_FORMAT_H265 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
2018-08-04 23:45:31 +00:00
m_StreamConfig . fps ) & &
2018-08-10 02:37:49 +00:00
m_Preferences . videoDecoderSelection = = StreamingPreferences : : VDS_AUTO ) {
2018-07-27 02:26:22 +00:00
if ( hevcForced ) {
2018-08-10 02:37:49 +00:00
emitLaunchWarning ( " Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance. " ) ;
2018-08-04 23:45:31 +00:00
}
else {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " This PC's GPU doesn't support HEVC decoding. " ) ;
2018-08-04 23:45:31 +00:00
m_StreamConfig . supportsHevc = false ;
2018-07-27 02:26:22 +00:00
}
}
if ( hevcForced ) {
if ( m_Computer - > maxLumaPixelsHEVC = = 0 ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " Your host PC GPU doesn't support HEVC. "
" A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming. " ) ;
2018-08-10 02:37:49 +00:00
// Moonlight-common-c will handle this case already, but we want
// to set this explicitly here so we can do our hardware acceleration
// check below.
m_StreamConfig . supportsHevc = false ;
2018-07-27 02:26:22 +00:00
}
2018-07-13 09:28:10 +00:00
}
2018-07-09 03:53:24 +00:00
}
2018-06-28 08:44:43 +00:00
if ( m_StreamConfig . enableHdr ) {
// Turn HDR back off unless all criteria are met.
m_StreamConfig . enableHdr = false ;
// Check that the app supports HDR
if ( ! m_App . hdrSupported ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( m_App . name + " doesn't support HDR10. " ) ;
2018-06-28 08:44:43 +00:00
}
// Check that the server GPU supports HDR
else if ( ! ( m_Computer - > serverCodecModeSupport & 0x200 ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " Your host PC GPU doesn't support HDR streaming. "
" A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming. " ) ;
2018-06-28 08:44:43 +00:00
}
2018-07-13 09:28:10 +00:00
else if ( ! isHardwareDecodeAvailable ( m_Preferences . videoDecoderSelection ,
VIDEO_FORMAT_H265_MAIN10 ,
m_StreamConfig . width ,
2018-07-18 03:00:16 +00:00
m_StreamConfig . height ,
m_StreamConfig . fps ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming. " ) ;
2018-07-13 09:28:10 +00:00
}
2018-06-28 08:44:43 +00:00
else {
2018-07-13 09:28:10 +00:00
// TODO: Also validate display capabilites
2018-06-28 08:44:43 +00:00
// Validation successful so HDR is good to go
m_StreamConfig . enableHdr = true ;
}
}
if ( m_StreamConfig . width > = 3840 ) {
2018-07-09 03:53:24 +00:00
// Only allow 4K on GFE 3.x+
if ( m_Computer - > gfeVersion . isNull ( ) | | m_Computer - > gfeVersion . startsWith ( " 2. " ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " GeForce Experience 3.0 or higher is required for 4K streaming. " ) ;
2018-07-09 03:53:24 +00:00
m_StreamConfig . width = 1920 ;
m_StreamConfig . height = 1080 ;
2018-06-28 08:44:43 +00:00
}
2018-07-09 03:53:24 +00:00
// This list is sorted from least to greatest
else if ( m_Computer - > displayModes . last ( ) . width < 3840 | |
( m_Computer - > displayModes . last ( ) . refreshRate < 60 & & m_StreamConfig . fps > = 60 ) ) {
2018-08-04 23:05:37 +00:00
emitLaunchWarning ( " Your host PC GPU doesn't support 4K streaming. "
" A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming. " ) ;
2018-07-09 03:53:24 +00:00
m_StreamConfig . width = 1920 ;
m_StreamConfig . height = 1080 ;
2018-06-28 08:44:43 +00:00
}
}
2018-08-31 04:09:31 +00:00
// Test that audio hardware is functional
m_AudioDisabled = ! testAudio ( m_StreamConfig . audioConfiguration ) ;
if ( m_AudioDisabled ) {
emitLaunchWarning ( " Failed to open audio device. Audio will be unavailable during this session. " ) ;
}
2018-08-10 02:37:49 +00:00
if ( m_Preferences . videoDecoderSelection = = StreamingPreferences : : VDS_FORCE_HARDWARE & &
! isHardwareDecodeAvailable ( m_Preferences . videoDecoderSelection ,
m_StreamConfig . supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
m_StreamConfig . fps ) ) {
if ( m_Preferences . videoCodecConfig = = StreamingPreferences : : VCC_AUTO ) {
emit displayLaunchError ( " Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU. " ) ;
}
else {
emit displayLaunchError ( " Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec. " ) ;
}
// Fail the launch, because we won't manage to get a decoder for the actual stream
return false ;
}
2018-08-25 20:36:54 +00:00
// Add the capability flags from the chosen decoder/renderer
m_VideoCallbacks . capabilities | = getDecoderCapabilities ( m_Preferences . videoDecoderSelection ,
m_StreamConfig . supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264 ,
m_StreamConfig . width ,
m_StreamConfig . height ,
m_StreamConfig . fps ) ;
2018-07-07 23:30:26 +00:00
return true ;
2018-06-28 08:44:43 +00:00
}
2018-07-09 07:09:06 +00:00
class DeferredSessionCleanupTask : public QRunnable
{
void run ( ) override
{
// Finish cleanup of the connection state
LiStopConnection ( ) ;
// Allow another session to start now that we're cleaned up
Session : : s_ActiveSession = nullptr ;
Session : : s_ActiveSessionSemaphore . release ( ) ;
}
} ;
2018-07-20 23:01:22 +00:00
void Session : : getWindowDimensions ( bool fullScreen ,
int & x , int & y ,
int & width , int & height )
{
int displayIndex = 0 ;
2018-08-17 05:25:14 +00:00
if ( m_Window ! = nullptr ) {
displayIndex = SDL_GetWindowDisplayIndex ( m_Window ) ;
SDL_assert ( displayIndex > = 0 ) ;
}
2018-08-05 21:55:26 +00:00
// If there's a display matching this exact resolution, pick that
// one (for native full-screen streaming). Otherwise, assume
// display 0 for now. TODO: Default to the screen that the Qt window is on
2018-08-17 05:25:14 +00:00
else if ( fullScreen ) {
2018-08-05 21:55:26 +00:00
for ( int i = 0 ; i < SDL_GetNumVideoDisplays ( ) ; i + + ) {
SDL_DisplayMode mode ;
if ( SDL_GetCurrentDisplayMode ( i , & mode ) = = 0 & &
m_ActiveVideoWidth = = mode . w & &
m_ActiveVideoHeight = = mode . h ) {
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Found exact resolution match on display: %d " ,
i ) ;
displayIndex = i ;
break ;
}
}
}
2018-08-17 05:25:14 +00:00
SDL_Rect usableBounds ;
if ( SDL_GetDisplayUsableBounds ( displayIndex , & usableBounds ) = = 0 ) {
width = usableBounds . w ;
height = usableBounds . h ;
x = usableBounds . x ;
y = usableBounds . y ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
if ( m_Window ! = nullptr ) {
int top , left , bottom , right ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
if ( SDL_GetWindowBordersSize ( m_Window , & top , & left , & bottom , & right ) < 0 ) {
SDL_LogWarn ( SDL_LOG_CATEGORY_APPLICATION ,
" Unable to get window border size " ) ;
return ;
}
2018-07-23 01:28:17 +00:00
2018-08-17 05:25:14 +00:00
x + = left ;
y + = top ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
width - = left + right ;
height - = top + bottom ;
// If the stream window can fit within the usable drawing area with 1:1
// scaling, do that rather than filling the screen.
if ( m_StreamConfig . width < width & & m_StreamConfig . height < height ) {
width = m_StreamConfig . width ;
height = m_StreamConfig . height ;
2018-07-21 02:18:55 +00:00
}
2018-07-20 23:01:22 +00:00
}
2018-08-17 05:25:14 +00:00
}
else {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_GetDisplayUsableBounds() failed: %s " ,
SDL_GetError ( ) ) ;
2018-07-20 23:01:22 +00:00
2018-08-17 05:25:14 +00:00
width = m_StreamConfig . width ;
height = m_StreamConfig . height ;
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY ( displayIndex ) ;
2018-07-20 23:01:22 +00:00
}
}
void Session : : toggleFullscreen ( )
{
2018-09-04 02:17:34 +00:00
bool fullScreen = ! ( SDL_GetWindowFlags ( m_Window ) & m_FullScreenFlag ) ;
2018-07-20 23:01:22 +00:00
int x , y , width , height ;
2018-07-21 02:18:55 +00:00
// If we're leaving full screen, toggle out before setting our size and position
// that way we have accurate display size metrics (not the size our stream changed it to).
if ( ! fullScreen ) {
SDL_SetWindowFullscreen ( m_Window , 0 ) ;
2018-08-04 22:30:44 +00:00
SDL_SetWindowResizable ( m_Window , SDL_TRUE ) ;
2018-07-21 02:18:55 +00:00
}
2018-07-20 23:01:22 +00:00
getWindowDimensions ( fullScreen , x , y , width , height ) ;
2018-07-23 01:40:15 +00:00
SDL_SetWindowPosition ( m_Window , x , y ) ;
2018-07-20 23:01:22 +00:00
SDL_SetWindowSize ( m_Window , width , height ) ;
2018-07-21 02:18:55 +00:00
if ( fullScreen ) {
2018-08-04 22:30:44 +00:00
SDL_SetWindowResizable ( m_Window , SDL_FALSE ) ;
2018-09-04 02:17:34 +00:00
SDL_SetWindowFullscreen ( m_Window , m_FullScreenFlag ) ;
2018-07-21 02:18:55 +00:00
}
2018-07-20 23:01:22 +00:00
}
2018-06-28 08:44:43 +00:00
void Session : : exec ( )
{
2018-07-07 23:30:26 +00:00
// Check for validation errors/warnings and emit
// signals for them, if appropriate
if ( ! validateLaunch ( ) ) {
return ;
}
// Manually pump the UI thread for the view
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
2018-07-09 07:09:06 +00:00
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore . acquire ( ) ;
2018-06-28 08:44:43 +00:00
// We're now active
s_ActiveSession = this ;
// Initialize the gamepad code with our preferences
StreamingPreferences prefs ;
SdlInputHandler inputHandler ( prefs . multiController ) ;
// The UI should have ensured the old game was already quit
// if we decide to stream a different game.
Q_ASSERT ( m_Computer - > currentGameId = = 0 | |
m_Computer - > currentGameId = = m_App . id ) ;
2018-08-06 01:09:35 +00:00
bool enableGameOptimizations = false ;
for ( const NvDisplayMode & mode : m_Computer - > displayModes ) {
if ( mode . width = = m_StreamConfig . width & &
mode . height = = m_StreamConfig . height ) {
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Found host supported resolution: %dx%d " ,
mode . width , mode . height ) ;
enableGameOptimizations = prefs . gameOptimizations ;
break ;
}
}
2018-06-28 09:10:31 +00:00
try {
2018-07-07 23:30:26 +00:00
NvHTTP http ( m_Computer - > activeAddress ) ;
2018-06-28 09:10:31 +00:00
if ( m_Computer - > currentGameId ! = 0 ) {
http . resumeApp ( & m_StreamConfig ) ;
}
else {
http . launchApp ( m_App . id , & m_StreamConfig ,
2018-08-06 01:09:35 +00:00
enableGameOptimizations ,
2018-06-28 09:10:31 +00:00
prefs . playAudioOnHost ,
inputHandler . getAttachedGamepadMask ( ) ) ;
}
} catch ( const GfeHttpResponseException & e ) {
2018-07-07 23:30:26 +00:00
emit displayLaunchError ( e . toQString ( ) ) ;
2018-07-09 07:09:06 +00:00
s_ActiveSessionSemaphore . release ( ) ;
2018-06-28 09:10:31 +00:00
return ;
2018-06-28 08:44:43 +00:00
}
2018-07-21 01:15:46 +00:00
SDL_assert ( ! SDL_WasInit ( SDL_INIT_VIDEO ) ) ;
if ( SDL_InitSubSystem ( SDL_INIT_VIDEO ) ! = 0 ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s " ,
SDL_GetError ( ) ) ;
emit displayLaunchError ( QString : : fromLocal8Bit ( SDL_GetError ( ) ) ) ;
s_ActiveSessionSemaphore . release ( ) ;
return ;
}
2018-06-28 08:44:43 +00:00
QByteArray hostnameStr = m_Computer - > activeAddress . toLatin1 ( ) ;
QByteArray siAppVersion = m_Computer - > appVersion . toLatin1 ( ) ;
SERVER_INFORMATION hostInfo ;
hostInfo . address = hostnameStr . data ( ) ;
hostInfo . serverInfoAppVersion = siAppVersion . data ( ) ;
// Older GFE versions didn't have this field
QByteArray siGfeVersion ;
if ( ! m_Computer - > gfeVersion . isNull ( ) ) {
siGfeVersion = m_Computer - > gfeVersion . toLatin1 ( ) ;
}
if ( ! siGfeVersion . isNull ( ) ) {
hostInfo . serverInfoGfeVersion = siGfeVersion . data ( ) ;
}
int err = LiStartConnection ( & hostInfo , & m_StreamConfig , & k_ConnCallbacks ,
2018-08-31 04:09:31 +00:00
& m_VideoCallbacks ,
m_AudioDisabled ? nullptr : & k_AudioCallbacks ,
2018-06-28 08:44:43 +00:00
NULL , 0 , NULL , 0 ) ;
if ( err ! = 0 ) {
// We already displayed an error dialog in the stage failure
// listener.
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-08-16 03:35:11 +00:00
s_ActiveSessionSemaphore . release ( ) ;
2018-06-28 08:44:43 +00:00
return ;
}
2018-07-07 23:30:26 +00:00
// Pump the message loop to update the UI
emit connectionStarted ( ) ;
2018-06-28 08:44:43 +00:00
QCoreApplication : : processEvents ( QEventLoop : : ExcludeUserInputEvents ) ;
2018-07-21 02:55:07 +00:00
int x , y , width , height ;
2018-09-04 02:17:34 +00:00
getWindowDimensions ( m_Preferences . windowMode ! = StreamingPreferences : : WM_WINDOWED ,
2018-07-21 02:55:07 +00:00
x , y , width , height ) ;
m_Window = SDL_CreateWindow ( " Moonlight " ,
x ,
y ,
width ,
height ,
2018-08-17 05:25:14 +00:00
0 ) ;
2018-07-21 02:55:07 +00:00
if ( ! m_Window ) {
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_CreateWindow() failed: %s " ,
SDL_GetError ( ) ) ;
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
2018-08-16 03:35:11 +00:00
s_ActiveSessionSemaphore . release ( ) ;
2018-07-21 02:55:07 +00:00
return ;
}
// For non-full screen windows, call getWindowDimensions()
// again after creating a window to allow it to account
// for window chrome size.
2018-09-04 02:17:34 +00:00
if ( m_Preferences . windowMode = = StreamingPreferences : : WM_WINDOWED ) {
2018-07-21 02:55:07 +00:00
getWindowDimensions ( false , x , y , width , height ) ;
SDL_SetWindowPosition ( m_Window , x , y ) ;
SDL_SetWindowSize ( m_Window , width , height ) ;
2018-08-04 22:30:44 +00:00
// Passing SDL_WINDOW_RESIZABLE to set this during window
// creation causes our window to be full screen for some reason
SDL_SetWindowResizable ( m_Window , SDL_TRUE ) ;
2018-07-21 02:55:07 +00:00
}
2018-08-17 05:25:14 +00:00
else {
// Update the window display mode based on our current monitor
SDL_DisplayMode mode ;
if ( SDL_GetDesktopDisplayMode ( SDL_GetWindowDisplayIndex ( m_Window ) , & mode ) = = 0 ) {
SDL_SetWindowDisplayMode ( m_Window , & mode ) ;
}
// Enter full screen
2018-09-04 02:17:34 +00:00
SDL_SetWindowFullscreen ( m_Window , m_FullScreenFlag ) ;
2018-08-17 05:25:14 +00:00
}
2018-07-08 04:52:20 +00:00
2018-07-23 00:07:45 +00:00
QSvgRenderer svgIconRenderer ( QString ( " :/res/moonlight.svg " ) ) ;
QImage svgImage ( ICON_SIZE , ICON_SIZE , QImage : : Format_RGBA8888 ) ;
svgImage . fill ( 0 ) ;
QPainter svgPainter ( & svgImage ) ;
svgIconRenderer . render ( & svgPainter ) ;
SDL_Surface * iconSurface = SDL_CreateRGBSurfaceWithFormatFrom ( ( void * ) svgImage . constBits ( ) ,
svgImage . width ( ) ,
svgImage . height ( ) ,
32 ,
4 * svgImage . width ( ) ,
SDL_PIXELFORMAT_RGBA32 ) ;
2018-07-23 00:42:31 +00:00
# ifndef Q_OS_DARWIN
2018-07-23 00:07:45 +00:00
// Other platforms seem to preserve our Qt icon when creating a new window
if ( iconSurface ! = nullptr ) {
SDL_SetWindowIcon ( m_Window , iconSurface ) ;
}
# endif
2018-07-22 23:21:15 +00:00
# ifndef QT_DEBUG
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
2018-08-11 21:19:42 +00:00
if ( m_Preferences . fullScreen ) {
SDL_SetRelativeMouseMode ( SDL_TRUE ) ;
}
2018-07-22 23:21:15 +00:00
# endif
2018-06-28 08:44:43 +00:00
2018-07-31 05:44:19 +00:00
// Stop text input. SDL enables it by default
// when we initialize the video subsystem, but this
// causes an IME popup when certain keys are held down
// on macOS.
SDL_StopTextInput ( ) ;
2018-07-13 09:28:10 +00:00
// Disable the screen saver
SDL_DisableScreenSaver ( ) ;
2018-08-21 04:36:23 +00:00
# ifdef Q_OS_WIN32
// HACK: SDL doesn't call this, so we must do so to disable the
// screensaver when we're in windowed mode. DirectX will disable
// it for us when we're in full-screen exclusive mode.
SetThreadExecutionState ( ES_CONTINUOUS | ES_DISPLAY_REQUIRED ) ;
# endif
2018-08-14 05:23:05 +00:00
// Set timer resolution to 1 ms on Windows for greater
// sleep precision and more accurate callback timing.
SDL_SetHint ( SDL_HINT_TIMER_RESOLUTION , " 1 " ) ;
2018-07-17 04:25:59 +00:00
// Raise the priority of the main thread, since it handles
// time-sensitive video rendering
if ( SDL_SetThreadPriority ( SDL_THREAD_PRIORITY_HIGH ) < 0 ) {
SDL_LogWarn ( SDL_LOG_CATEGORY_APPLICATION ,
" Unable to set main thread to high priority: %s " ,
SDL_GetError ( ) ) ;
}
2018-06-28 08:44:43 +00:00
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
SDL_Event event ;
2018-08-02 01:26:50 +00:00
for ( ; ; ) {
// We explicitly use SDL_PollEvent() and SDL_Delay() because
// SDL_WaitEvent() has an internal SDL_Delay(10) inside which
// blocks this thread too long for high polling rate mice and high
// refresh rate displays.
if ( ! SDL_PollEvent ( & event ) ) {
SDL_Delay ( 1 ) ;
continue ;
}
2018-06-28 08:44:43 +00:00
switch ( event . type ) {
case SDL_QUIT :
SDL_LogInfo ( SDL_LOG_CATEGORY_APPLICATION ,
" Quit event received " ) ;
2018-07-09 07:09:06 +00:00
goto DispatchDeferredCleanup ;
2018-07-08 04:52:20 +00:00
case SDL_USEREVENT : {
SDL_Event nextEvent ;
SDL_assert ( event . user . code = = SDL_CODE_FRAME_READY ) ;
// Drop any earlier frames
while ( SDL_PeepEvents ( & nextEvent ,
1 ,
SDL_GETEVENT ,
SDL_USEREVENT ,
SDL_USEREVENT ) = = 1 ) {
2018-07-18 03:00:16 +00:00
m_VideoDecoder - > dropFrame ( & event . user ) ;
2018-07-08 04:52:20 +00:00
event = nextEvent ;
}
// Render the last frame
2018-07-18 03:00:16 +00:00
m_VideoDecoder - > renderFrame ( & event . user ) ;
2018-07-08 04:52:20 +00:00
break ;
}
2018-07-20 22:31:57 +00:00
2018-07-21 02:37:54 +00:00
case SDL_WINDOWEVENT :
2018-09-01 21:31:37 +00:00
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
if ( event . window . event = = SDL_WINDOWEVENT_FOCUS_LOST ) {
SDL_SetRelativeMouseMode ( SDL_FALSE ) ;
}
2018-07-21 07:16:03 +00:00
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
// seem to fire when switching from windowed to full-screen on X11.
if ( event . window . event ! = SDL_WINDOWEVENT_SIZE_CHANGED & & event . window . event ! = SDL_WINDOWEVENT_SHOWN ) {
2018-07-21 02:37:54 +00:00
break ;
}
2018-07-21 02:55:07 +00:00
// Fall through
2018-07-20 22:31:57 +00:00
case SDL_RENDER_DEVICE_RESET :
case SDL_RENDER_TARGETS_RESET :
2018-08-17 05:25:14 +00:00
2018-08-19 08:19:23 +00:00
SDL_AtomicLock ( & m_DecoderLock ) ;
// Destroy the old decoder
delete m_VideoDecoder ;
// Flush any other pending window events that could
// send us back here immediately
SDL_PumpEvents ( ) ;
SDL_FlushEvent ( SDL_WINDOWEVENT ) ;
2018-08-17 05:25:14 +00:00
// Update the window display mode based on our current monitor
SDL_DisplayMode mode ;
if ( SDL_GetDesktopDisplayMode ( SDL_GetWindowDisplayIndex ( m_Window ) , & mode ) = = 0 ) {
SDL_SetWindowDisplayMode ( m_Window , & mode ) ;
}
2018-08-19 08:19:23 +00:00
// Now that the old decoder is dead, flush any events it may
// have queued to reset itself (if this reset was the result
// of state loss).
SDL_PumpEvents ( ) ;
SDL_FlushEvent ( SDL_RENDER_DEVICE_RESET ) ;
SDL_FlushEvent ( SDL_RENDER_TARGETS_RESET ) ;
2018-07-20 22:31:57 +00:00
2018-08-16 06:20:56 +00:00
// Choose a new decoder (hopefully the same one, but possibly
2018-07-20 22:31:57 +00:00
// not if a GPU was removed or something).
if ( ! chooseDecoder ( m_Preferences . videoDecoderSelection ,
m_Window , m_ActiveVideoFormat , m_ActiveVideoWidth ,
m_ActiveVideoHeight , m_ActiveVideoFrameRate ,
2018-08-21 05:25:19 +00:00
m_Preferences . enableVsync ,
2018-07-20 22:31:57 +00:00
s_ActiveSession - > m_VideoDecoder ) ) {
SDL_AtomicUnlock ( & m_DecoderLock ) ;
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" Failed to recreate decoder after reset " ) ;
2018-08-12 05:49:36 +00:00
emit displayLaunchError ( " Unable to initialize video decoder. Please check your streaming settings and try again. " ) ;
2018-07-20 22:31:57 +00:00
goto DispatchDeferredCleanup ;
}
// Request an IDR frame to complete the reset
m_NeedsIdr = true ;
SDL_AtomicUnlock ( & m_DecoderLock ) ;
break ;
2018-06-28 08:44:43 +00:00
case SDL_KEYUP :
case SDL_KEYDOWN :
inputHandler . handleKeyEvent ( & event . key ) ;
break ;
case SDL_MOUSEBUTTONDOWN :
case SDL_MOUSEBUTTONUP :
inputHandler . handleMouseButtonEvent ( & event . button ) ;
break ;
case SDL_MOUSEMOTION :
inputHandler . handleMouseMotionEvent ( & event . motion ) ;
break ;
case SDL_MOUSEWHEEL :
inputHandler . handleMouseWheelEvent ( & event . wheel ) ;
break ;
case SDL_CONTROLLERAXISMOTION :
inputHandler . handleControllerAxisEvent ( & event . caxis ) ;
break ;
case SDL_CONTROLLERBUTTONDOWN :
case SDL_CONTROLLERBUTTONUP :
inputHandler . handleControllerButtonEvent ( & event . cbutton ) ;
break ;
case SDL_CONTROLLERDEVICEADDED :
case SDL_CONTROLLERDEVICEREMOVED :
inputHandler . handleControllerDeviceEvent ( & event . cdevice ) ;
break ;
}
}
SDL_LogError ( SDL_LOG_CATEGORY_APPLICATION ,
" SDL_WaitEvent() failed: %s " ,
SDL_GetError ( ) ) ;
2018-07-09 07:09:06 +00:00
DispatchDeferredCleanup :
2018-08-21 04:36:23 +00:00
# ifdef Q_OS_WIN32
// HACK: See comment above
SetThreadExecutionState ( ES_CONTINUOUS ) ;
# endif
2018-07-09 07:09:06 +00:00
// Uncapture the mouse and hide the window immediately,
// so we can return to the Qt GUI ASAP.
SDL_SetRelativeMouseMode ( SDL_FALSE ) ;
2018-07-13 09:28:10 +00:00
SDL_EnableScreenSaver ( ) ;
2018-08-14 05:23:05 +00:00
SDL_SetHint ( SDL_HINT_TIMER_RESOLUTION , " 0 " ) ;
2018-07-09 07:09:06 +00:00
2018-07-18 03:00:16 +00:00
// Destroy the decoder, since this must be done on the main thread
SDL_AtomicLock ( & m_DecoderLock ) ;
delete m_VideoDecoder ;
m_VideoDecoder = nullptr ;
SDL_AtomicUnlock ( & m_DecoderLock ) ;
2018-07-21 01:15:46 +00:00
SDL_DestroyWindow ( m_Window ) ;
2018-07-23 00:07:45 +00:00
if ( iconSurface ! = nullptr ) {
SDL_FreeSurface ( iconSurface ) ;
}
2018-07-21 01:15:46 +00:00
SDL_QuitSubSystem ( SDL_INIT_VIDEO ) ;
SDL_assert ( ! SDL_WasInit ( SDL_INIT_VIDEO ) ) ;
2018-07-09 07:09:06 +00:00
// Cleanup can take a while, so dispatch it to a worker thread.
// When it is complete, it will release our s_ActiveSessionSemaphore
// reference.
QThreadPool : : globalInstance ( ) - > start ( new DeferredSessionCleanupTask ( ) ) ;
2018-06-28 08:44:43 +00:00
}
2018-07-09 07:09:06 +00:00