mirror of
https://github.com/moonlight-stream/moonlight-common-c.git
synced 2025-08-18 01:15:46 +00:00
522 lines
18 KiB
C
522 lines
18 KiB
C
#include "Limelight-internal.h"
|
|
|
|
static int stage = STAGE_NONE;
|
|
static ConnListenerConnectionTerminated originalTerminationCallback;
|
|
static bool alreadyTerminated;
|
|
static PLT_THREAD terminationCallbackThread;
|
|
static int terminationCallbackErrorCode;
|
|
|
|
// Common globals
|
|
char* RemoteAddrString;
|
|
struct sockaddr_storage RemoteAddr;
|
|
SOCKADDR_LEN RemoteAddrLen;
|
|
int AppVersionQuad[4];
|
|
STREAM_CONFIGURATION StreamConfig;
|
|
CONNECTION_LISTENER_CALLBACKS ListenerCallbacks;
|
|
DECODER_RENDERER_CALLBACKS VideoCallbacks;
|
|
AUDIO_RENDERER_CALLBACKS AudioCallbacks;
|
|
int NegotiatedVideoFormat;
|
|
volatile bool ConnectionInterrupted;
|
|
bool HighQualitySurroundSupported;
|
|
bool HighQualitySurroundEnabled;
|
|
OPUS_MULTISTREAM_CONFIGURATION NormalQualityOpusConfig;
|
|
OPUS_MULTISTREAM_CONFIGURATION HighQualityOpusConfig;
|
|
int OriginalVideoBitrate;
|
|
int AudioPacketDuration;
|
|
bool AudioEncryptionEnabled;
|
|
bool ReferenceFrameInvalidationSupported;
|
|
uint16_t RtspPortNumber;
|
|
uint16_t ControlPortNumber;
|
|
uint16_t AudioPortNumber;
|
|
uint16_t VideoPortNumber;
|
|
SS_PING AudioPingPayload;
|
|
SS_PING VideoPingPayload;
|
|
uint32_t SunshineFeatureFlags;
|
|
|
|
// Connection stages
|
|
static const char* stageNames[STAGE_MAX] = {
|
|
"none",
|
|
"platform initialization",
|
|
"name resolution",
|
|
"audio stream initialization",
|
|
"RTSP handshake",
|
|
"control stream initialization",
|
|
"video stream initialization",
|
|
"input stream initialization",
|
|
"control stream establishment",
|
|
"video stream establishment",
|
|
"audio stream establishment",
|
|
"input stream establishment"
|
|
};
|
|
|
|
// Get the name of the current stage based on its number
|
|
const char* LiGetStageName(int stage) {
|
|
return stageNames[stage];
|
|
}
|
|
|
|
// Interrupt a pending connection attempt. This interruption happens asynchronously
|
|
// so it is not safe to start another connection before LiStartConnection() returns.
|
|
void LiInterruptConnection(void) {
|
|
// Signal anyone waiting on the global interrupted flag
|
|
ConnectionInterrupted = true;
|
|
}
|
|
|
|
// Stop the connection by undoing the step at the current stage and those before it
|
|
void LiStopConnection(void) {
|
|
// Disable termination callbacks now
|
|
alreadyTerminated = true;
|
|
|
|
// Set the interrupted flag
|
|
LiInterruptConnection();
|
|
|
|
if (stage == STAGE_INPUT_STREAM_START) {
|
|
Limelog("Stopping input stream...");
|
|
stopInputStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_AUDIO_STREAM_START) {
|
|
Limelog("Stopping audio stream...");
|
|
stopAudioStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_VIDEO_STREAM_START) {
|
|
Limelog("Stopping video stream...");
|
|
stopVideoStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_CONTROL_STREAM_START) {
|
|
Limelog("Stopping control stream...");
|
|
stopControlStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_INPUT_STREAM_INIT) {
|
|
Limelog("Cleaning up input stream...");
|
|
destroyInputStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_VIDEO_STREAM_INIT) {
|
|
Limelog("Cleaning up video stream...");
|
|
destroyVideoStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_CONTROL_STREAM_INIT) {
|
|
Limelog("Cleaning up control stream...");
|
|
destroyControlStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_RTSP_HANDSHAKE) {
|
|
// Nothing to do
|
|
stage--;
|
|
}
|
|
if (stage == STAGE_AUDIO_STREAM_INIT) {
|
|
Limelog("Cleaning up audio stream...");
|
|
destroyAudioStream();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
if (stage == STAGE_NAME_RESOLUTION) {
|
|
// Nothing to do
|
|
stage--;
|
|
}
|
|
if (stage == STAGE_PLATFORM_INIT) {
|
|
Limelog("Cleaning up platform...");
|
|
cleanupPlatform();
|
|
stage--;
|
|
Limelog("done\n");
|
|
}
|
|
LC_ASSERT(stage == STAGE_NONE);
|
|
|
|
if (RemoteAddrString != NULL) {
|
|
free(RemoteAddrString);
|
|
RemoteAddrString = NULL;
|
|
}
|
|
}
|
|
|
|
static void terminationCallbackThreadFunc(void* context)
|
|
{
|
|
// Invoke the client's termination callback
|
|
originalTerminationCallback(terminationCallbackErrorCode);
|
|
}
|
|
|
|
// This shim callback runs the client's connectionTerminated() callback on a
|
|
// separate thread. This is neccessary because other internal threads directly
|
|
// invoke this callback. That can result in a deadlock if the client
|
|
// calls LiStopConnection() in the callback when the cleanup code
|
|
// attempts to join the thread that the termination callback (and LiStopConnection)
|
|
// is running on.
|
|
static void ClInternalConnectionTerminated(int errorCode)
|
|
{
|
|
int err;
|
|
|
|
// Avoid recursion and issuing multiple callbacks
|
|
if (alreadyTerminated || ConnectionInterrupted) {
|
|
return;
|
|
}
|
|
|
|
terminationCallbackErrorCode = errorCode;
|
|
alreadyTerminated = true;
|
|
|
|
// Invoke the termination callback on a separate thread
|
|
err = PltCreateThread("AsyncTerm", terminationCallbackThreadFunc, NULL, &terminationCallbackThread);
|
|
if (err != 0) {
|
|
// Nothing we can safely do here, so we'll just assert on debug builds
|
|
Limelog("Failed to create termination thread: %d\n", err);
|
|
LC_ASSERT(err == 0);
|
|
}
|
|
|
|
// Close the thread handle since we can never wait on it
|
|
PltCloseThread(&terminationCallbackThread);
|
|
}
|
|
|
|
static bool parseRtspPortNumberFromUrl(const char* rtspSessionUrl, uint16_t* port)
|
|
{
|
|
// If the session URL is not present, we will just use the well known port
|
|
if (rtspSessionUrl == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// Pick the last colon in the string to match the port number
|
|
char* portNumberStart = strrchr(rtspSessionUrl, ':');
|
|
if (portNumberStart == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// Skip the colon
|
|
portNumberStart++;
|
|
|
|
// Validate the port number
|
|
long int rawPort = strtol(portNumberStart, NULL, 10);
|
|
if (rawPort <= 0 || rawPort > 65535) {
|
|
return false;
|
|
}
|
|
|
|
*port = (uint16_t)rawPort;
|
|
return true;
|
|
}
|
|
|
|
// Starts the connection to the streaming machine
|
|
int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION streamConfig, PCONNECTION_LISTENER_CALLBACKS clCallbacks,
|
|
PDECODER_RENDERER_CALLBACKS drCallbacks, PAUDIO_RENDERER_CALLBACKS arCallbacks, void* renderContext, int drFlags,
|
|
void* audioContext, int arFlags) {
|
|
int err;
|
|
|
|
if (drCallbacks != NULL && (drCallbacks->capabilities & CAPABILITY_PULL_RENDERER) && drCallbacks->submitDecodeUnit) {
|
|
Limelog("CAPABILITY_PULL_RENDERER cannot be set with a submitDecodeUnit callback\n");
|
|
LC_ASSERT(false);
|
|
err = -1;
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (drCallbacks != NULL && (drCallbacks->capabilities & CAPABILITY_PULL_RENDERER) && (drCallbacks->capabilities & CAPABILITY_DIRECT_SUBMIT)) {
|
|
Limelog("CAPABILITY_PULL_RENDERER and CAPABILITY_DIRECT_SUBMIT cannot be set together\n");
|
|
LC_ASSERT(false);
|
|
err = -1;
|
|
goto Cleanup;
|
|
}
|
|
|
|
if (serverInfo->serverCodecModeSupport == 0) {
|
|
Limelog("serverCodecModeSupport field in SERVER_INFORMATION must be set!\n");
|
|
LC_ASSERT(false);
|
|
err = -1;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// Extract the appversion from the supplied string
|
|
if (extractVersionQuadFromString(serverInfo->serverInfoAppVersion,
|
|
AppVersionQuad) < 0) {
|
|
Limelog("Invalid appversion string: %s\n", serverInfo->serverInfoAppVersion);
|
|
err = -1;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// Replace missing callbacks with placeholders
|
|
fixupMissingCallbacks(&drCallbacks, &arCallbacks, &clCallbacks);
|
|
memcpy(&VideoCallbacks, drCallbacks, sizeof(VideoCallbacks));
|
|
memcpy(&AudioCallbacks, arCallbacks, sizeof(AudioCallbacks));
|
|
|
|
#ifdef LC_DEBUG_RECORD_MODE
|
|
// Install the pass-through recorder callbacks
|
|
setRecorderCallbacks(&VideoCallbacks, &AudioCallbacks);
|
|
#endif
|
|
|
|
// Hook the termination callback so we can avoid issuing a termination callback
|
|
// after LiStopConnection() is called.
|
|
//
|
|
// Initialize ListenerCallbacks before anything that could call Limelog().
|
|
originalTerminationCallback = clCallbacks->connectionTerminated;
|
|
memcpy(&ListenerCallbacks, clCallbacks, sizeof(ListenerCallbacks));
|
|
ListenerCallbacks.connectionTerminated = ClInternalConnectionTerminated;
|
|
|
|
NegotiatedVideoFormat = 0;
|
|
memcpy(&StreamConfig, streamConfig, sizeof(StreamConfig));
|
|
OriginalVideoBitrate = streamConfig->bitrate;
|
|
RemoteAddrString = strdup(serverInfo->address);
|
|
|
|
// The values in RTSP SETUP will be used to populate these.
|
|
VideoPortNumber = 0;
|
|
ControlPortNumber = 0;
|
|
AudioPortNumber = 0;
|
|
|
|
// Parse RTSP port number from RTSP session URL
|
|
if (!parseRtspPortNumberFromUrl(serverInfo->rtspSessionUrl, &RtspPortNumber)) {
|
|
// Use the well known port if parsing fails
|
|
RtspPortNumber = 48010;
|
|
|
|
Limelog("RTSP port: %u (RTSP URL parsing failed)\n", RtspPortNumber);
|
|
}
|
|
else {
|
|
Limelog("RTSP port: %u\n", RtspPortNumber);
|
|
}
|
|
|
|
alreadyTerminated = false;
|
|
ConnectionInterrupted = false;
|
|
|
|
// Validate the audio configuration
|
|
if (MAGIC_BYTE_FROM_AUDIO_CONFIG(StreamConfig.audioConfiguration) != 0xCA ||
|
|
CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration) > AUDIO_CONFIGURATION_MAX_CHANNEL_COUNT) {
|
|
Limelog("Invalid audio configuration specified\n");
|
|
err = -1;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// FEC only works in 16 byte chunks, so we must round down
|
|
// the given packet size to the nearest multiple of 16.
|
|
StreamConfig.packetSize -= StreamConfig.packetSize % 16;
|
|
|
|
if (StreamConfig.packetSize == 0) {
|
|
Limelog("Invalid packet size specified\n");
|
|
err = -1;
|
|
goto Cleanup;
|
|
}
|
|
|
|
// Height must not be odd or NVENC will fail to initialize
|
|
if (StreamConfig.height & 0x1) {
|
|
Limelog("Encoder height must not be odd. Rounding %d to %d\n",
|
|
StreamConfig.height,
|
|
StreamConfig.height & ~0x1);
|
|
StreamConfig.height = StreamConfig.height & ~0x1;
|
|
}
|
|
|
|
// Dimensions over 4096 are only supported with HEVC on NVENC
|
|
if (!(StreamConfig.supportedVideoFormats & ~VIDEO_FORMAT_MASK_H264) &&
|
|
(StreamConfig.width > 4096 || StreamConfig.height > 4096)) {
|
|
Limelog("WARNING: Streaming at resolutions above 4K using H.264 will likely fail! Trying anyway!\n");
|
|
}
|
|
// Dimensions over 8192 aren't supported at all (even on Turing)
|
|
else if (StreamConfig.width > 8192 || StreamConfig.height > 8192) {
|
|
Limelog("WARNING: Streaming at resolutions above 8K will likely fail! Trying anyway!\n");
|
|
}
|
|
|
|
// Reference frame invalidation doesn't seem to work with resolutions much
|
|
// higher than 1440p. I haven't figured out a pattern to indicate which
|
|
// resolutions will work and which won't, but we can at least exclude
|
|
// 4K from RFI to avoid significant persistent artifacts after frame loss.
|
|
if (StreamConfig.width == 3840 && StreamConfig.height == 2160 &&
|
|
(VideoCallbacks.capabilities & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC) &&
|
|
!IS_SUNSHINE()) {
|
|
Limelog("Disabling reference frame invalidation for 4K streaming with GFE\n");
|
|
VideoCallbacks.capabilities &= ~CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
|
}
|
|
|
|
Limelog("Initializing platform...");
|
|
ListenerCallbacks.stageStarting(STAGE_PLATFORM_INIT);
|
|
err = initializePlatform();
|
|
if (err != 0) {
|
|
Limelog("failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_PLATFORM_INIT, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_PLATFORM_INIT);
|
|
ListenerCallbacks.stageComplete(STAGE_PLATFORM_INIT);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Resolving host name...");
|
|
ListenerCallbacks.stageStarting(STAGE_NAME_RESOLUTION);
|
|
LC_ASSERT(RtspPortNumber != 0);
|
|
if (RtspPortNumber != 48010) {
|
|
// If we have an alternate RTSP port, use that as our test port. The host probably
|
|
// isn't listening on 47989 or 47984 anyway, since they're using alternate ports.
|
|
err = resolveHostName(serverInfo->address, AF_UNSPEC, RtspPortNumber, &RemoteAddr, &RemoteAddrLen);
|
|
if (err != 0) {
|
|
// Sleep for a second and try again. It's possible that we've attempt to connect
|
|
// before the host has gotten around to listening on the RTSP port. Give it some
|
|
// time before retrying.
|
|
PltSleepMs(1000);
|
|
err = resolveHostName(serverInfo->address, AF_UNSPEC, RtspPortNumber, &RemoteAddr, &RemoteAddrLen);
|
|
}
|
|
}
|
|
else {
|
|
// We use TCP 47984 and 47989 first here because we know those should always be listening
|
|
// on hosts using the standard ports.
|
|
//
|
|
// TCP 48010 is a last resort because:
|
|
// a) it's not always listening and there's a race between listen() on the host and our connect()
|
|
// b) it's not used at all by certain host versions which perform RTSP over ENet
|
|
err = resolveHostName(serverInfo->address, AF_UNSPEC, 47984, &RemoteAddr, &RemoteAddrLen);
|
|
if (err != 0) {
|
|
err = resolveHostName(serverInfo->address, AF_UNSPEC, 47989, &RemoteAddr, &RemoteAddrLen);
|
|
}
|
|
if (err != 0) {
|
|
err = resolveHostName(serverInfo->address, AF_UNSPEC, 48010, &RemoteAddr, &RemoteAddrLen);
|
|
}
|
|
}
|
|
if (err != 0) {
|
|
Limelog("failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_NAME_RESOLUTION, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_NAME_RESOLUTION);
|
|
ListenerCallbacks.stageComplete(STAGE_NAME_RESOLUTION);
|
|
Limelog("done\n");
|
|
|
|
// If STREAM_CFG_AUTO was requested, determine the streamingRemotely value
|
|
// now that we have resolved the target address and impose the video packet
|
|
// size cap if required.
|
|
if (StreamConfig.streamingRemotely == STREAM_CFG_AUTO) {
|
|
if (isPrivateNetworkAddress(&RemoteAddr)) {
|
|
StreamConfig.streamingRemotely = STREAM_CFG_LOCAL;
|
|
}
|
|
else {
|
|
StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
|
|
|
if (StreamConfig.packetSize > 1024) {
|
|
// Cap packet size at 1024 for remote streaming to avoid
|
|
// MTU problems and fragmentation.
|
|
Limelog("Packet size capped at 1KB for remote streaming\n");
|
|
StreamConfig.packetSize = 1024;
|
|
}
|
|
}
|
|
}
|
|
|
|
Limelog("Initializing audio stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_AUDIO_STREAM_INIT);
|
|
err = initializeAudioStream();
|
|
if (err != 0) {
|
|
Limelog("failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_AUDIO_STREAM_INIT, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_AUDIO_STREAM_INIT);
|
|
ListenerCallbacks.stageComplete(STAGE_AUDIO_STREAM_INIT);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Starting RTSP handshake...");
|
|
ListenerCallbacks.stageStarting(STAGE_RTSP_HANDSHAKE);
|
|
err = performRtspHandshake(serverInfo);
|
|
if (err != 0) {
|
|
Limelog("failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_RTSP_HANDSHAKE, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_RTSP_HANDSHAKE);
|
|
ListenerCallbacks.stageComplete(STAGE_RTSP_HANDSHAKE);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Initializing control stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_CONTROL_STREAM_INIT);
|
|
err = initializeControlStream();
|
|
if (err != 0) {
|
|
Limelog("failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_CONTROL_STREAM_INIT, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_CONTROL_STREAM_INIT);
|
|
ListenerCallbacks.stageComplete(STAGE_CONTROL_STREAM_INIT);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Initializing video stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_VIDEO_STREAM_INIT);
|
|
initializeVideoStream();
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_VIDEO_STREAM_INIT);
|
|
ListenerCallbacks.stageComplete(STAGE_VIDEO_STREAM_INIT);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Initializing input stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_INPUT_STREAM_INIT);
|
|
initializeInputStream();
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_INPUT_STREAM_INIT);
|
|
ListenerCallbacks.stageComplete(STAGE_INPUT_STREAM_INIT);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Starting control stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_CONTROL_STREAM_START);
|
|
err = startControlStream();
|
|
if (err != 0) {
|
|
Limelog("failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_CONTROL_STREAM_START, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_CONTROL_STREAM_START);
|
|
ListenerCallbacks.stageComplete(STAGE_CONTROL_STREAM_START);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Starting video stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_VIDEO_STREAM_START);
|
|
err = startVideoStream(renderContext, drFlags);
|
|
if (err != 0) {
|
|
Limelog("Video stream start failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_VIDEO_STREAM_START, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_VIDEO_STREAM_START);
|
|
ListenerCallbacks.stageComplete(STAGE_VIDEO_STREAM_START);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Starting audio stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_AUDIO_STREAM_START);
|
|
err = startAudioStream(audioContext, arFlags);
|
|
if (err != 0) {
|
|
Limelog("Audio stream start failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_AUDIO_STREAM_START, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_AUDIO_STREAM_START);
|
|
ListenerCallbacks.stageComplete(STAGE_AUDIO_STREAM_START);
|
|
Limelog("done\n");
|
|
|
|
Limelog("Starting input stream...");
|
|
ListenerCallbacks.stageStarting(STAGE_INPUT_STREAM_START);
|
|
err = startInputStream();
|
|
if (err != 0) {
|
|
Limelog("Input stream start failed: %d\n", err);
|
|
ListenerCallbacks.stageFailed(STAGE_INPUT_STREAM_START, err);
|
|
goto Cleanup;
|
|
}
|
|
stage++;
|
|
LC_ASSERT(stage == STAGE_INPUT_STREAM_START);
|
|
ListenerCallbacks.stageComplete(STAGE_INPUT_STREAM_START);
|
|
Limelog("done\n");
|
|
|
|
// Wiggle the mouse a bit to wake the display up
|
|
LiSendMouseMoveEvent(1, 1);
|
|
PltSleepMs(10);
|
|
LiSendMouseMoveEvent(-1, -1);
|
|
PltSleepMs(10);
|
|
|
|
ListenerCallbacks.connectionStarted();
|
|
|
|
Cleanup:
|
|
if (err != 0) {
|
|
// Undo any work we've done here before failing
|
|
LiStopConnection();
|
|
}
|
|
return err;
|
|
}
|