-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathsession.cpp
More file actions
2482 lines (2186 loc) · 106 KB
/
session.cpp
File metadata and controls
2482 lines (2186 loc) · 106 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "session.h"
#include "settings/streamingpreferences.h"
#include "streaming/streamutils.h"
#include "backend/richpresencemanager.h"
#include <Limelight.h>
#include "SDL_compat.h"
#include "utils.h"
#ifdef HAVE_FFMPEG
#include "video/ffmpeg.h"
#endif
#ifdef HAVE_SLVIDEO
#include "video/slvid.h"
#endif
#ifdef Q_OS_WIN32
// Scaling the icon down on Win32 looks dreadful, so render at lower res
#define ICON_SIZE 32
#else
#define ICON_SIZE 64
#endif
// HACK: Remove once proper Dark Mode support lands in SDL
#ifdef Q_OS_WIN32
#include <SDL_syswm.h>
#include <dwmapi.h>
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE_OLD
#define DWMWA_USE_IMMERSIVE_DARK_MODE_OLD 19
#endif
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
#endif
#endif
#define SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER 100
#define SDL_CODE_GAMECONTROLLER_RUMBLE 101
#define SDL_CODE_GAMECONTROLLER_RUMBLE_TRIGGERS 102
#define SDL_CODE_GAMECONTROLLER_SET_MOTION_EVENT_STATE 103
#define SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED 104
#define SDL_CODE_GAMECONTROLLER_SET_ADAPTIVE_TRIGGERS 105
#include <openssl/rand.h>
#include <QtEndian>
#include <QCoreApplication>
#include <QThreadPool>
#include <QSvgRenderer>
#include <QPainter>
#include <QImage>
#include <QGuiApplication>
#include <QCursor>
#include <QScreen>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QQuickOpenGLUtils>
#endif
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting,
nullptr,
Session::clStageFailed,
nullptr,
Session::clConnectionTerminated,
Session::clLogMessage,
Session::clRumble,
Session::clConnectionStatusUpdate,
Session::clSetHdrMode,
Session::clRumbleTriggers,
Session::clSetMotionEventState,
Session::clSetControllerLED,
Session::clSetAdaptiveTriggers
};
Session* Session::s_ActiveSession;
QSemaphore Session::s_ActiveSessionSemaphore(1);
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.
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
}
void Session::clStageFailed(int stage, int errorCode)
{
// Perform the port test now, while we're on the async connection thread and not blocking the UI.
unsigned int portFlags = LiGetPortFlagsFromStage(stage);
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
char failingPorts[128];
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts));
}
void Session::clConnectionTerminated(int errorCode)
{
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
// Display the termination dialog if this was not intended
switch (errorCode) {
case ML_ERROR_GRACEFUL_TERMINATION:
break;
case ML_ERROR_NO_VIDEO_TRAFFIC:
s_ActiveSession->m_UnexpectedTermination = true;
char ports[128];
SDL_assert(portFlags != 0);
LiStringifyPortFlags(portFlags, ", ", ports, sizeof(ports));
emit s_ActiveSession->displayLaunchError(tr("No video received from host.") + "\n\n"+
tr("Check your firewall and port forwarding rules for port(s): %1").arg(ports));
break;
case ML_ERROR_NO_VIDEO_FRAME:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError(tr("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection."));
break;
case ML_ERROR_PROTECTED_CONTENT:
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError(tr("Something went wrong on your host PC when starting the stream.") + "\n\n" +
tr("Make sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC."));
break;
case ML_ERROR_FRAME_CONVERSION:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError(tr("The host PC reported a fatal video encoding error.") + "\n\n" +
tr("Try disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution."));
break;
default:
s_ActiveSession->m_UnexpectedTermination = true;
// We'll assume large errors are hex values
bool hexError = qAbs(errorCode) > 1000;
emit s_ActiveSession->displayLaunchError(tr("Connection terminated") + "\n\n" +
tr("Error code: %1").arg(errorCode, hexError ? 8 : 0, hexError ? 16 : 10, QChar('0')));
break;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %d",
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);
}
void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// We push an event for the main thread to handle in order to properly synchronize
// with the removal of game controllers that could result in our game controller
// going away during this callback.
SDL_Event rumbleEvent = {};
rumbleEvent.type = SDL_USEREVENT;
rumbleEvent.user.code = SDL_CODE_GAMECONTROLLER_RUMBLE;
rumbleEvent.user.data1 = (void*)(uintptr_t)controllerNumber;
rumbleEvent.user.data2 = (void*)(uintptr_t)((lowFreqMotor << 16) | highFreqMotor);
SDL_PushEvent(&rumbleEvent);
}
void Session::clConnectionStatusUpdate(int connectionStatus)
{
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Connection status update: %d",
connectionStatus);
if (!s_ActiveSession->m_Preferences->connectionWarnings) {
return;
}
if (s_ActiveSession->m_MouseEmulationRefCount > 0) {
// Don't display the overlay if mouse emulation is already using it
return;
}
switch (connectionStatus)
{
case CONN_STATUS_POOR:
s_ActiveSession->m_OverlayManager.updateOverlayText(Overlay::OverlayStatusUpdate,
s_ActiveSession->m_StreamConfig.bitrate > 5000 ?
"Slow connection to PC\nReduce your bitrate" : "Poor connection to PC");
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
break;
case CONN_STATUS_OKAY:
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
break;
}
}
void Session::clSetHdrMode(bool enabled)
{
// If we're in the process of recreating our decoder when we get
// this callback, we'll drop it. The main thread will make the
// callback when it finishes creating the new decoder.
if (SDL_TryLockMutex(s_ActiveSession->m_DecoderLock) == 0) {
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
if (decoder != nullptr) {
decoder->setHdrMode(enabled);
}
SDL_UnlockMutex(s_ActiveSession->m_DecoderLock);
}
}
void Session::clRumbleTriggers(uint16_t controllerNumber, uint16_t leftTrigger, uint16_t rightTrigger)
{
// We push an event for the main thread to handle in order to properly synchronize
// with the removal of game controllers that could result in our game controller
// going away during this callback.
SDL_Event rumbleEvent = {};
rumbleEvent.type = SDL_USEREVENT;
rumbleEvent.user.code = SDL_CODE_GAMECONTROLLER_RUMBLE_TRIGGERS;
rumbleEvent.user.data1 = (void*)(uintptr_t)controllerNumber;
rumbleEvent.user.data2 = (void*)(uintptr_t)((leftTrigger << 16) | rightTrigger);
SDL_PushEvent(&rumbleEvent);
}
void Session::clSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
{
// We push an event for the main thread to handle in order to properly synchronize
// with the removal of game controllers that could result in our game controller
// going away during this callback.
SDL_Event setMotionEventStateEvent = {};
setMotionEventStateEvent.type = SDL_USEREVENT;
setMotionEventStateEvent.user.code = SDL_CODE_GAMECONTROLLER_SET_MOTION_EVENT_STATE;
setMotionEventStateEvent.user.data1 = (void*)(uintptr_t)controllerNumber;
setMotionEventStateEvent.user.data2 = (void*)(uintptr_t)((motionType << 16) | reportRateHz);
SDL_PushEvent(&setMotionEventStateEvent);
}
void Session::clSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
{
// We push an event for the main thread to handle in order to properly synchronize
// with the removal of game controllers that could result in our game controller
// going away during this callback.
SDL_Event setControllerLEDEvent = {};
setControllerLEDEvent.type = SDL_USEREVENT;
setControllerLEDEvent.user.code = SDL_CODE_GAMECONTROLLER_SET_CONTROLLER_LED;
setControllerLEDEvent.user.data1 = (void*)(uintptr_t)controllerNumber;
setControllerLEDEvent.user.data2 = (void*)(uintptr_t)(r << 16 | g << 8 | b);
SDL_PushEvent(&setControllerLEDEvent);
}
void Session::clSetAdaptiveTriggers(uint16_t controllerNumber, uint8_t eventFlags, uint8_t typeLeft, uint8_t typeRight, uint8_t *left, uint8_t *right){
// We push an event for the main thread to handle in order to properly synchronize
// with the removal of game controllers that could result in our game controller
// going away during this callback.
SDL_Event setControllerLEDEvent = {};
setControllerLEDEvent.type = SDL_USEREVENT;
setControllerLEDEvent.user.code = SDL_CODE_GAMECONTROLLER_SET_ADAPTIVE_TRIGGERS;
setControllerLEDEvent.user.data1 = (void*)(uintptr_t)controllerNumber;
// Based on the following SDL code:
// https://github.com/libsdl-org/SDL/blob/120c76c84bbce4c1bfed4e9eb74e10678bd83120/test/testgamecontroller.c#L286-L307
DualSenseOutputReport *state = (DualSenseOutputReport *) SDL_malloc(sizeof(DualSenseOutputReport));
SDL_zero(*state);
state->validFlag0 = (eventFlags & DS_EFFECT_RIGHT_TRIGGER) | (eventFlags & DS_EFFECT_LEFT_TRIGGER);
state->rightTriggerEffectType = typeRight;
SDL_memcpy(state->rightTriggerEffect, right, sizeof(state->rightTriggerEffect));
state->leftTriggerEffectType = typeLeft;
SDL_memcpy(state->leftTriggerEffect, left, sizeof(state->leftTriggerEffect));
setControllerLEDEvent.user.data2 = (void *) state;
SDL_PushEvent(&setControllerLEDEvent);
}
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
{
DECODER_PARAMETERS params;
// We should never have vsync enabled for test-mode.
// It introduces unnecessary delay for renderers that may
// block while waiting for a backbuffer swap.
SDL_assert(!enableVsync || !testOnly);
params.width = width;
params.height = height;
params.frameRate = frameRate;
params.videoFormat = videoFormat;
params.window = window;
params.enableVsync = enableVsync;
params.enableFramePacing = enableFramePacing;
params.testOnly = testOnly;
params.vds = vds;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-sync %s",
enableVsync ? "enabled" : "disabled");
#ifdef HAVE_SLVIDEO
chosenDecoder = new SLVideoDecoder(testOnly);
if (chosenDecoder->initialize(¶ms)) {
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
#ifdef HAVE_FFMPEG
chosenDecoder = new FFmpegVideoDecoder(testOnly);
if (chosenDecoder->initialize(¶ms)) {
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;
}
#endif
#if !defined(HAVE_FFMPEG) && !defined(HAVE_SLVIDEO)
#error No video decoding libraries available!
#endif
// 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)
{
s_ActiveSession->m_ActiveVideoFormat = videoFormat;
s_ActiveSession->m_ActiveVideoWidth = width;
s_ActiveSession->m_ActiveVideoHeight = height;
s_ActiveSession->m_ActiveVideoFrameRate = frameRate;
// 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).
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Video stream is %dx%dx%d (format 0x%x)",
width, height, frameRate, videoFormat);
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). If we can't acquire it,
// that means the decoder is about to be destroyed, so we can
// safely return DR_OK and wait for the IDR frame request by
// the decoder reinitialization code.
if (SDL_TryLockMutex(s_ActiveSession->m_DecoderLock) == 0) {
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
if (decoder != nullptr) {
int ret = decoder->submitDecodeUnit(du);
SDL_UnlockMutex(s_ActiveSession->m_DecoderLock);
return ret;
}
else {
SDL_UnlockMutex(s_ActiveSession->m_DecoderLock);
return DR_OK;
}
}
else {
// Decoder is going away. Ignore anything coming in until
// the lock is released.
return DR_OK;
}
}
void Session::getDecoderInfo(SDL_Window* window,
bool& isHardwareAccelerated, bool& isFullScreenOnly,
bool& isHdrSupported, QSize& maxResolution)
{
IVideoDecoder* decoder;
// Since AV1 support on the host side is in its infancy, let's not consider
// _only_ a working AV1 decoder to be acceptable and still show the warning
// dialog indicating lack of hardware decoding support.
// Try an HEVC Main10 decoder first to see if we have HDR support
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
window, VIDEO_FORMAT_H265_MAIN10, 1920, 1080, 60,
false, false, true, decoder)) {
isHardwareAccelerated = decoder->isHardwareAccelerated();
isFullScreenOnly = decoder->isAlwaysFullScreen();
isHdrSupported = decoder->isHdrSupported();
maxResolution = decoder->getDecoderMaxResolution();
delete decoder;
return;
}
// Try an AV1 Main10 decoder next to see if we have HDR support
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
window, VIDEO_FORMAT_AV1_MAIN10, 1920, 1080, 60,
false, false, true, decoder)) {
// If we've got a working AV1 Main 10-bit decoder, we'll enable the HDR checkbox
// but we will still continue probing to get other attributes for HEVC or H.264
// decoders. See the AV1 comment at the top of the function for more info.
isHdrSupported = decoder->isHdrSupported();
delete decoder;
}
else {
// If we found no hardware decoders with HDR, check for a renderer
// that supports HDR rendering with software decoded frames.
if (chooseDecoder(StreamingPreferences::VDS_FORCE_SOFTWARE,
window, VIDEO_FORMAT_H265_MAIN10, 1920, 1080, 60,
false, false, true, decoder) ||
chooseDecoder(StreamingPreferences::VDS_FORCE_SOFTWARE,
window, VIDEO_FORMAT_AV1_MAIN10, 1920, 1080, 60,
false, false, true, decoder)) {
isHdrSupported = decoder->isHdrSupported();
delete decoder;
}
else {
// We weren't compiled with an HDR-capable renderer or we don't
// have the required GPU driver support for any HDR renderers.
isHdrSupported = false;
}
}
// Try a regular hardware accelerated HEVC decoder now
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
window, VIDEO_FORMAT_H265, 1920, 1080, 60,
false, false, true, decoder)) {
isHardwareAccelerated = decoder->isHardwareAccelerated();
isFullScreenOnly = decoder->isAlwaysFullScreen();
maxResolution = decoder->getDecoderMaxResolution();
delete decoder;
return;
}
#if 0 // See AV1 comment at the top of this function
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
window, VIDEO_FORMAT_AV1_MAIN8, 1920, 1080, 60,
false, false, true, decoder)) {
isHardwareAccelerated = decoder->isHardwareAccelerated();
isFullScreenOnly = decoder->isAlwaysFullScreen();
maxResolution = decoder->getDecoderMaxResolution();
delete decoder;
return;
}
#endif
// If we still didn't find a hardware decoder, try H.264 now.
// This will fall back to software decoding, so it should always work.
if (chooseDecoder(StreamingPreferences::VDS_AUTO,
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
false, false, true, decoder)) {
isHardwareAccelerated = decoder->isHardwareAccelerated();
isFullScreenOnly = decoder->isAlwaysFullScreen();
maxResolution = decoder->getDecoderMaxResolution();
delete decoder;
return;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to find ANY working H.264 or HEVC decoder!");
}
Session::DecoderAvailability
Session::getDecoderAvailability(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
{
IVideoDecoder* decoder;
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) {
return DecoderAvailability::None;
}
bool hw = decoder->isHardwareAccelerated();
delete decoder;
return hw ? DecoderAvailability::Hardware : DecoderAvailability::Software;
}
bool Session::populateDecoderProperties(SDL_Window* window)
{
IVideoDecoder* decoder;
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
window,
m_SupportedVideoFormats.first(),
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps,
false, false, true, decoder)) {
return false;
}
m_VideoCallbacks.capabilities = decoder->getDecoderCapabilities();
if (m_VideoCallbacks.capabilities & CAPABILITY_PULL_RENDERER) {
// It is an error to pass a push callback when in pull mode
m_VideoCallbacks.submitDecodeUnit = nullptr;
}
else {
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
}
if (Utils::getEnvironmentVariableOverride("COLOR_SPACE_OVERRIDE", &m_StreamConfig.colorSpace)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using colorspace override: %d",
m_StreamConfig.colorSpace);
}
else {
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
}
if (Utils::getEnvironmentVariableOverride("COLOR_RANGE_OVERRIDE", &m_StreamConfig.colorRange)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using color range override: %d",
m_StreamConfig.colorRange);
}
else {
m_StreamConfig.colorRange = decoder->getDecoderColorRange();
}
if (decoder->isAlwaysFullScreen()) {
m_IsFullScreen = true;
}
delete decoder;
return true;
}
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
: m_Preferences(preferences ? preferences : StreamingPreferences::get()),
m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()),
m_Computer(computer),
m_App(app),
m_Window(nullptr),
m_VideoDecoder(nullptr),
m_DecoderLock(SDL_CreateMutex()),
m_AudioMuted(false),
m_QtWindow(nullptr),
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
m_InputHandler(nullptr),
m_MouseEmulationRefCount(0),
m_FlushingWindowEventsRef(0),
m_ShouldExit(false),
m_AsyncConnectionSuccess(false),
m_PortTestResults(0),
m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr),
m_AudioSampleCount(0),
m_DropAudioEndTime(0),
m_LastInputTime(0),
m_InactivityTimeoutMs((m_Preferences->inactivityTimeoutMinutes > 0 ?
m_Preferences->inactivityTimeoutMinutes : 30) * 60 * 1000),
m_InactivityTimeoutEnabled(m_Preferences->inactivityTimeoutEnabled)
{
}
Session::~Session()
{
// NB: This may not get destroyed for a long time! Don't put any non-trivial cleanup here.
// Use Session::exec() or DeferredSessionCleanupTask instead.
SDL_DestroyMutex(m_DecoderLock);
}
void Session::resetInactivityTimer()
{
if (m_InactivityTimeoutEnabled) {
m_LastInputTime = SDL_GetTicks();
}
}
void Session::checkInactivityTimeout()
{
if (m_InactivityTimeoutEnabled) {
Uint32 currentTime = SDL_GetTicks();
if (currentTime - m_LastInputTime > m_InactivityTimeoutMs) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Disconnecting due to %d minutes of inactivity",
m_InactivityTimeoutMs / 60000);
// Push a quit event to trigger cleanup
SDL_Event quitEvent;
quitEvent.type = SDL_QUIT;
quitEvent.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&quitEvent);
}
}
}
bool Session::initialize(QQuickWindow* qtWindow)
{
m_QtWindow = qtWindow;
#ifdef Q_OS_DARWIN
if (qEnvironmentVariableIntValue("I_WANT_BUGGY_FULLSCREEN") == 0) {
// If we have a notch and the user specified one of the two native display modes
// (notched or notchless), override the fullscreen mode to ensure it works as expected.
// - SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES=0 will place the video underneath the notch
// - SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES=1 will place the video below the notch
bool shouldUseFullScreenSpaces = m_Preferences->windowMode != StreamingPreferences::WM_FULLSCREEN;
SDL_DisplayMode desktopMode;
SDL_Rect safeArea;
for (int displayIndex = 0; StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode, &safeArea); displayIndex++) {
// Check if this display has a notch (safeArea != desktopMode)
if (desktopMode.h != safeArea.h || desktopMode.w != safeArea.w) {
// Check if we're trying to stream at the full native resolution (including notch)
if (m_Preferences->width == desktopMode.w && m_Preferences->height == desktopMode.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Overriding default fullscreen mode for native fullscreen resolution");
shouldUseFullScreenSpaces = false;
break;
}
else if (m_Preferences->width == safeArea.w && m_Preferences->height == safeArea.h) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Overriding default fullscreen mode for native safe area resolution");
shouldUseFullScreenSpaces = true;
break;
}
}
}
// Using modesetting on modern versions of macOS is extremely unreliable
// and leads to hangs, deadlocks, and other nasty stuff. The only time
// people seem to use it is to get the full screen on notched Macs,
// which setting SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES=1 also accomplishes
// with much less headache.
//
// https://github.com/moonlight-stream/moonlight-qt/issues/973
// https://github.com/moonlight-stream/moonlight-qt/issues/999
// https://github.com/moonlight-stream/moonlight-qt/issues/1211
// https://github.com/moonlight-stream/moonlight-qt/issues/1218
SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, shouldUseFullScreenSpaces ? "1" : "0");
}
#endif
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return false;
}
LiInitializeStreamConfiguration(&m_StreamConfig);
m_StreamConfig.width = m_Preferences->width;
m_StreamConfig.height = m_Preferences->height;
int x, y, width, height;
getWindowDimensions(x, y, width, height);
// Create a hidden window to use for decoder initialization tests
SDL_Window* testWindow = SDL_CreateWindow("", x, y, width, height,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
testWindow = SDL_CreateWindow("", x, y, width, height, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
}
qInfo() << "Server GPU:" << m_Computer->gpuModel;
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
LiInitializeVideoCallbacks(&m_VideoCallbacks);
m_VideoCallbacks.setup = drSetup;
m_StreamConfig.fps = m_Preferences->fps;
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
#ifndef STEAM_LINK
// Opt-in to all encryption features if we detect that the platform
// has AES cryptography acceleration instructions and more than 2 cores.
if (StreamUtils::hasFastAes() && SDL_GetCPUCount() > 2) {
m_StreamConfig.encryptionFlags = ENCFLG_ALL;
}
else {
// Enable audio encryption as long as we're not on Steam Link.
// That hardware can hardly handle Opus decoding at all.
m_StreamConfig.encryptionFlags = ENCFLG_AUDIO;
}
#endif
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Video bitrate: %d kbps",
m_StreamConfig.bitrate);
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);
switch (m_Preferences->audioConfig)
{
case StreamingPreferences::AC_STEREO:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
break;
case StreamingPreferences::AC_51_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
break;
case StreamingPreferences::AC_71_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
break;
}
LiInitializeAudioCallbacks(&m_AudioCallbacks);
m_AudioCallbacks.init = arInit;
m_AudioCallbacks.cleanup = arCleanup;
m_AudioCallbacks.decodeAndPlaySample = arDecodeAndPlaySample;
m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio channel count: %d",
CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio channel mask: %X",
CHANNEL_MASK_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
// Start with all codecs and profiles in priority order
m_SupportedVideoFormats.append(VIDEO_FORMAT_AV1_HIGH10_444);
m_SupportedVideoFormats.append(VIDEO_FORMAT_AV1_MAIN10);
m_SupportedVideoFormats.append(VIDEO_FORMAT_H265_REXT10_444);
m_SupportedVideoFormats.append(VIDEO_FORMAT_H265_MAIN10);
m_SupportedVideoFormats.append(VIDEO_FORMAT_AV1_HIGH8_444);
m_SupportedVideoFormats.append(VIDEO_FORMAT_AV1_MAIN8);
m_SupportedVideoFormats.append(VIDEO_FORMAT_H265_REXT8_444);
m_SupportedVideoFormats.append(VIDEO_FORMAT_H265);
m_SupportedVideoFormats.append(VIDEO_FORMAT_H264_HIGH8_444);
m_SupportedVideoFormats.append(VIDEO_FORMAT_H264);
switch (m_Preferences->videoCodecConfig)
{
case StreamingPreferences::VCC_AUTO:
{
// Codecs are checked in order of ascending decode complexity to ensure
// the the deprioritized list prefers lighter codecs for software decoding
// H.264 is already the lowest priority codec, so we don't need to do
// any probing for deprioritization for it here.
auto hevcDA = getDecoderAvailability(testWindow,
m_Preferences->videoDecoderSelection,
m_Preferences->enableYUV444 ?
(m_Preferences->enableHdr ? VIDEO_FORMAT_H265_REXT10_444 : VIDEO_FORMAT_H265_REXT8_444) :
(m_Preferences->enableHdr ? VIDEO_FORMAT_H265_MAIN10 : VIDEO_FORMAT_H265),
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
if (hevcDA == DecoderAvailability::None && m_Preferences->enableHdr) {
// Remove all 10-bit HEVC profiles
m_SupportedVideoFormats.removeByMask(VIDEO_FORMAT_MASK_H265 & VIDEO_FORMAT_MASK_10BIT);
// Check if we have 10-bit AV1 support
auto av1DA = getDecoderAvailability(testWindow,
m_Preferences->videoDecoderSelection,
m_Preferences->enableYUV444 ? VIDEO_FORMAT_AV1_HIGH10_444 : VIDEO_FORMAT_AV1_MAIN10,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
if (av1DA == DecoderAvailability::None) {
// Remove all 10-bit AV1 profiles
m_SupportedVideoFormats.removeByMask(VIDEO_FORMAT_MASK_AV1 & VIDEO_FORMAT_MASK_10BIT);
// There are no available 10-bit profiles, so reprobe for 8-bit HEVC
// and we'll proceed as normal for an SDR streaming scenario.
SDL_assert(!(m_SupportedVideoFormats & VIDEO_FORMAT_MASK_10BIT));
hevcDA = getDecoderAvailability(testWindow,
m_Preferences->videoDecoderSelection,
m_Preferences->enableYUV444 ? VIDEO_FORMAT_H265_REXT8_444 : VIDEO_FORMAT_H265,
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps);
}
}
if (hevcDA != DecoderAvailability::Hardware) {
// Deprioritize HEVC unless the user forced software decoding and enabled HDR.
// We need HEVC in that case because we cannot support 10-bit content with H.264,
// which would ordinarily be prioritized for software decoding performance.
if (m_Preferences->videoDecoderSelection != StreamingPreferences::VDS_FORCE_SOFTWARE || !m_Preferences->enableHdr) {
m_SupportedVideoFormats.deprioritizeByMask(VIDEO_FORMAT_MASK_H265);
}
}
#if 0
// TODO: Determine if AV1 is better depending on the decoder
if (getDecoderAvailability(testWindow,
m_Preferences->videoDecoderSelection,
m_Preferences->enableYUV444 ?
(m_Preferences->enableHdr ? VIDEO_FORMAT_AV1_HIGH10_444 : VIDEO_FORMAT_AV1_HIGH8_444) :
(m_Preferences->enableHdr ? VIDEO_FORMAT_AV1_MAIN10 : VIDEO_FORMAT_AV1_MAIN8),
m_StreamConfig.width,
m_StreamConfig.height,
m_StreamConfig.fps) != DecoderAvailability::Hardware) {
// Deprioritize AV1 unless we can't hardware decode HEVC and have HDR enabled.
// We want to keep AV1 at the top of the list for HDR with software decoding
// because dav1d is higher performance than FFmpeg's HEVC software decoder.
if (hevcDA == DecoderAvailability::Hardware || !m_Preferences->enableHdr) {
m_SupportedVideoFormats.deprioritizeByMask(VIDEO_FORMAT_MASK_AV1);
}
}
#else
// Deprioritize AV1 unless we can't hardware decode HEVC, and have HDR enabled
// or we're on Windows or a non-x86 Linux/BSD.
//
// Normally, we'd assume hardware that can't decode HEVC definitely can't decode
// AV1 either, and we wouldn't even bother probing for AV1 support. However, some
// Windows business systems have HEVC support disabled in firmware from the factory,
// yet they can still decode AV1 in hardware. To avoid falling back to H.264 on
// these systems, we don't deprioritize AV1. This firmware-based HEVC licensing
// behavior seems to be unique to Windows, and Linux on the same system is able
// to decode HEVC in hardware normally using VAAPI.
// https://www.reddit.com/r/GeForceNOW/comments/1omsckt/psa_be_wary_of_purchasing_dell_computers_with/
//
// Some embedded Linux platforms have incomplete V4L2 decoding support which can
// lead to unusual cases where a system might support H.264 and AV1 but not HEVC,
// even if the underlying hardware supports all three. RK3588 is an example of
// such a SoC. To handle this situation, we will also probe for AV1 if we're on
// a non-x86 non-macOS UNIX system.
//
// We want to keep AV1 at the top of the list for HDR with software decoding
// because dav1d is higher performance than FFmpeg's HEVC software decoder.
if (hevcDA == DecoderAvailability::Hardware
#if !defined(Q_OS_WIN32) && (!(defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)) || defined(Q_PROCESSOR_X86))
|| !m_Preferences->enableHdr
#endif
) {
m_SupportedVideoFormats.deprioritizeByMask(VIDEO_FORMAT_MASK_AV1);
}
#endif
#ifdef Q_OS_DARWIN
{
// Prior to GFE 3.11, GFE did not allow us to constrain
// the number of reference frames, so we have to fixup the SPS
// to allow decoding via VideoToolbox on macOS. Since we don't
// have fixup code for HEVC, just avoid it if GFE is too old.
QVector<int> gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion);
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
gfeVersion[0] < 3 ||
(gfeVersion[0] == 3 && gfeVersion[1] < 11)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Disabling HEVC on macOS due to old GFE version");
m_SupportedVideoFormats.removeByMask(VIDEO_FORMAT_MASK_H265);
}
}
#endif
break;
}
case StreamingPreferences::VCC_FORCE_H264:
m_SupportedVideoFormats.removeByMask(~VIDEO_FORMAT_MASK_H264);
break;
case StreamingPreferences::VCC_FORCE_HEVC:
case StreamingPreferences::VCC_FORCE_HEVC_HDR_DEPRECATED:
m_SupportedVideoFormats.removeByMask(~VIDEO_FORMAT_MASK_H265);
break;
case StreamingPreferences::VCC_FORCE_AV1:
// We'll try to fall back to HEVC first if AV1 fails. We'd rather not fall back
// straight to H.264 if the user asked for AV1 and the host doesn't support it.
m_SupportedVideoFormats.removeByMask(~(VIDEO_FORMAT_MASK_AV1 | VIDEO_FORMAT_MASK_H265));
break;
}
// NB: Since deprioritization puts codecs in reverse order (at the bottom of the list),
// we want to deprioritize for the most critical attributes last to ensure they are the
// lowest priority codecs during server negotiation. Here we do that with YUV 4:4:4 and
// HDR to ensure we never pick a codec profile that doesn't meet the user's requirement
// if we can avoid it.
// Mask off YUV 4:4:4 codecs if the option is not enabled
if (!m_Preferences->enableYUV444) {
m_SupportedVideoFormats.removeByMask(VIDEO_FORMAT_MASK_YUV444);
}
else {
// Deprioritize YUV 4:2:0 codecs if the user wants YUV 4:4:4
//
// NB: Since this happens first before deprioritizing HDR, we will
// pick a YUV 4:4:4 profile instead of a 10-bit profile if they
// aren't both available together for any codec.
m_SupportedVideoFormats.deprioritizeByMask(~VIDEO_FORMAT_MASK_YUV444);
}
// Mask off 10-bit codecs if HDR is not enabled
if (!m_Preferences->enableHdr) {
m_SupportedVideoFormats.removeByMask(VIDEO_FORMAT_MASK_10BIT);
}
else {
// Deprioritize 8-bit codecs if HDR is enabled
m_SupportedVideoFormats.deprioritizeByMask(~VIDEO_FORMAT_MASK_10BIT);
}
switch (m_Preferences->windowMode)
{
default:
// Normally we'd default to fullscreen desktop when starting in windowed
// mode, but in the case of a slow GPU, we want to use real fullscreen
// to allow the display to assist with the video scaling work.
if (WMUtils::isGpuSlow()) {
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
break;
}
// Fall-through
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
// Only use full-screen desktop mode if we're running a desktop environment
if (WMUtils::isRunningDesktopEnvironment()) {
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
break;
}
// Fall-through
case StreamingPreferences::WM_FULLSCREEN:
#ifdef Q_OS_DARWIN
if (qEnvironmentVariableIntValue("I_WANT_BUGGY_FULLSCREEN") == 0) {
// Don't use "real" fullscreen on macOS by default. See comments above.
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
}
else {
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
}
#else
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
#endif
break;
}
#if !SDL_VERSION_ATLEAST(2, 0, 11)
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
// GUI environment. Force the session to use windowed mode (which won't
// really matter anyway because the MMAL renderer always draws full-screen).
if (qgetenv("DESKTOP_SESSION") == "LXDE-pi") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Forcing windowed mode on LXDE-Pi");
m_FullScreenFlag = 0;
}
#endif
// Check for validation errors/warnings and emit
// signals for them, if appropriate
bool ret = validateLaunch(testWindow);
if (ret) {
// Video format is now locked in
m_StreamConfig.supportedVideoFormats = m_SupportedVideoFormats.front();
// Populate decoder-dependent properties.
// Must be done after validateLaunch() since m_StreamConfig is finalized.
ret = populateDecoderProperties(testWindow);
}
SDL_DestroyWindow(testWindow);
if (!ret) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
return true;
}
void Session::emitLaunchWarning(QString text)