#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; }