moonlight-common-c/src/RtspConnection.c
2024-02-02 22:55:53 -06:00

1365 lines
45 KiB
C

#include "Limelight-internal.h"
#include "Rtsp.h"
#define RTSP_CONNECT_TIMEOUT_SEC 10
#define RTSP_RECEIVE_TIMEOUT_SEC 15
#define RTSP_RETRY_DELAY_MS 500
static int currentSeqNumber;
static char rtspTargetUrl[256];
static char* sessionIdString;
static bool hasSessionId;
static int rtspClientVersion;
static char urlAddr[URLSAFESTRING_LEN];
static bool useEnet;
static char* controlStreamId;
static bool encryptedRtspEnabled;
static PPLT_CRYPTO_CONTEXT encryptionCtx;
static PPLT_CRYPTO_CONTEXT decryptionCtx;
static uint32_t encryptionSequenceNumber;
static SOCKET sock = INVALID_SOCKET;
static ENetHost* client;
static ENetPeer* peer;
#define CHAR_TO_INT(x) ((x) - '0')
#define CHAR_IS_DIGIT(x) ((x) >= '0' && (x) <= '9')
// Create RTSP Option
static POPTION_ITEM createOptionItem(char* option, char* content)
{
POPTION_ITEM item = malloc(sizeof(*item));
if (item == NULL) {
return NULL;
}
item->option = strdup(option);
if (item->option == NULL) {
free(item);
return NULL;
}
item->content = strdup(content);
if (item->content == NULL) {
free(item->option);
free(item);
return NULL;
}
item->next = NULL;
item->flags = FLAG_ALLOCATED_OPTION_FIELDS;
return item;
}
// Add an option to the RTSP Message
static bool addOption(PRTSP_MESSAGE msg, char* option, char* content)
{
POPTION_ITEM item = createOptionItem(option, content);
if (item == NULL) {
return false;
}
insertOption(&msg->options, item);
msg->flags |= FLAG_ALLOCATED_OPTION_ITEMS;
return true;
}
// Create an RTSP Request
static bool initializeRtspRequest(PRTSP_MESSAGE msg, char* command, char* target)
{
char sequenceNumberStr[16];
char clientVersionStr[16];
// FIXME: Hacked CSeq attribute due to RTSP parser bug
createRtspRequest(msg, NULL, 0, command, target, "RTSP/1.0",
0, NULL, NULL, 0);
snprintf(sequenceNumberStr, sizeof(sequenceNumberStr), "%d", currentSeqNumber++);
snprintf(clientVersionStr, sizeof(clientVersionStr), "%d", rtspClientVersion);
if (!addOption(msg, "CSeq", sequenceNumberStr) ||
!addOption(msg, "X-GS-ClientVersion", clientVersionStr) ||
(!useEnet && !addOption(msg, "Host", urlAddr))) {
freeMessage(msg);
return false;
}
return true;
}
#define ENCRYPTED_RTSP_BIT 0x80000000
typedef struct _ENC_RTSP_HEADER {
uint32_t typeAndLength; // BE
uint32_t sequenceNumber; // BE
uint8_t tag[16];
} ENC_RTSP_HEADER, *PENC_RTSP_HEADER;
static char* sealRtspMessage(PRTSP_MESSAGE request, int* messageLen) {
char* serializedMessage;
PENC_RTSP_HEADER encryptedMessage;
int plaintextLen;
bool success;
uint8_t iv[12] = { 0 };
serializedMessage = serializeRtspMessage(request, &plaintextLen);
if (serializedMessage == NULL) {
return NULL;
}
else if (!encryptedRtspEnabled) {
*messageLen = plaintextLen;
return serializedMessage;
}
encryptedMessage = (PENC_RTSP_HEADER)malloc(sizeof(ENC_RTSP_HEADER) + plaintextLen);
if (encryptedMessage == NULL) {
free(serializedMessage);
return NULL;
}
// Populate the IV in little endian byte order
encryptionSequenceNumber++;
iv[3] = (uint8_t)(encryptionSequenceNumber >> 24);
iv[2] = (uint8_t)(encryptionSequenceNumber >> 16);
iv[1] = (uint8_t)(encryptionSequenceNumber >> 8);
iv[0] = (uint8_t)(encryptionSequenceNumber >> 0);
// Set high bytes to something unique to ensure no IV collisions
iv[10] = (uint8_t)'C'; // Client originated
iv[11] = (uint8_t)'R'; // RTSP stream
encryptedMessage->typeAndLength = BE32(ENCRYPTED_RTSP_BIT | plaintextLen);
encryptedMessage->sequenceNumber = BE32(encryptionSequenceNumber);
success = PltEncryptMessage(encryptionCtx, ALGORITHM_AES_GCM, 0,
(uint8_t*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
iv, sizeof(iv),
encryptedMessage->tag, sizeof(encryptedMessage->tag),
(uint8_t*)serializedMessage, plaintextLen,
(uint8_t*)(encryptedMessage + 1), messageLen);
free(serializedMessage);
if (!success) {
free(encryptedMessage);
return NULL;
}
// The size returned from PltEncryptMessage() is the payload only
*messageLen += sizeof(ENC_RTSP_HEADER);
return (char*)encryptedMessage;
}
static bool unsealRtspMessage(char* rawMessage, int rawMessageLen, PRTSP_MESSAGE response) {
char* decryptedMessage;
int decryptedMessageLen;
bool success;
if (encryptedRtspEnabled) {
PENC_RTSP_HEADER encryptedMessage;
uint32_t seq;
uint8_t iv[12] = { 0 };
if (rawMessageLen <= (int)sizeof(ENC_RTSP_HEADER)) {
return false;
}
encryptedMessage = (PENC_RTSP_HEADER)rawMessage;
seq = BE32(encryptedMessage->sequenceNumber);
// Populate the IV in little endian byte order
iv[3] = (uint8_t)(seq >> 24);
iv[2] = (uint8_t)(seq >> 16);
iv[1] = (uint8_t)(seq >> 8);
iv[0] = (uint8_t)(seq >> 0);
// Set high bytes to something unique to ensure no IV collisions
iv[10] = (uint8_t)'H'; // Host originated
iv[11] = (uint8_t)'R'; // RTSP stream
decryptedMessageLen = rawMessageLen - sizeof(ENC_RTSP_HEADER);
decryptedMessage = (char*)malloc(decryptedMessageLen);
if (decryptedMessage == NULL) {
return false;
}
success = PltDecryptMessage(decryptionCtx, ALGORITHM_AES_GCM, 0,
(uint8_t*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
iv, sizeof(iv),
encryptedMessage->tag, sizeof(encryptedMessage->tag),
(uint8_t*)(encryptedMessage + 1), decryptedMessageLen,
(uint8_t*)decryptedMessage, &decryptedMessageLen);
if (!success) {
Limelog("Failed to decrypt RTSP response\n");
free(decryptedMessage);
return false;
}
}
else {
decryptedMessage = rawMessage;
decryptedMessageLen = rawMessageLen;
}
if (parseRtspMessage(response, decryptedMessage, decryptedMessageLen) == RTSP_ERROR_SUCCESS) {
success = true;
}
else {
Limelog("Failed to parse RTSP response\n");
success = false;
}
if (decryptedMessage != rawMessage) {
free(decryptedMessage);
}
return success;
}
// Send RTSP message and get response over ENet
static bool transactRtspMessageEnet(PRTSP_MESSAGE request, PRTSP_MESSAGE response, bool expectingPayload, int* error) {
ENetEvent event;
char* serializedMessage;
int messageLen;
int offset;
ENetPacket* packet;
char* payload;
int payloadLength;
bool ret;
char* responseBuffer;
// RTSP encryption is not supported using ENet due to our special handling
// of the payload below. Modern versions of Sunshine use TCP for RTSP.
LC_ASSERT(!encryptedRtspEnabled);
*error = -1;
ret = false;
responseBuffer = NULL;
// We're going to handle the payload separately, so temporarily set the payload to NULL
payload = request->payload;
payloadLength = request->payloadLength;
request->payload = NULL;
request->payloadLength = 0;
// Serialize the RTSP message into a message buffer
serializedMessage = serializeRtspMessage(request, &messageLen);
if (serializedMessage == NULL) {
goto Exit;
}
// Create the reliable packet that describes our outgoing message
packet = enet_packet_create(serializedMessage, messageLen, ENET_PACKET_FLAG_RELIABLE);
if (packet == NULL) {
goto Exit;
}
// Send the message
if (enet_peer_send(peer, 0, packet) < 0) {
enet_packet_destroy(packet);
goto Exit;
}
enet_host_flush(client);
// If we have a payload to send, we'll need to send that separately
if (payload != NULL) {
packet = enet_packet_create(payload, payloadLength, ENET_PACKET_FLAG_RELIABLE);
if (packet == NULL) {
goto Exit;
}
// Send the payload
if (enet_peer_send(peer, 0, packet) < 0) {
enet_packet_destroy(packet);
goto Exit;
}
enet_host_flush(client);
}
// Wait for a reply
if (serviceEnetHost(client, &event, RTSP_RECEIVE_TIMEOUT_SEC * 1000) <= 0 ||
event.type != ENET_EVENT_TYPE_RECEIVE) {
Limelog("Failed to receive RTSP reply: %d\n", LastSocketFail());
goto Exit;
}
responseBuffer = malloc(event.packet->dataLength);
if (responseBuffer == NULL) {
Limelog("Failed to allocate RTSP response buffer\n");
enet_packet_destroy(event.packet);
goto Exit;
}
// Copy the data out and destroy the packet
memcpy(responseBuffer, event.packet->data, event.packet->dataLength);
offset = (int) event.packet->dataLength;
enet_packet_destroy(event.packet);
// Wait for the payload if we're expecting some
if (expectingPayload) {
// The payload comes in a second packet
if (serviceEnetHost(client, &event, RTSP_RECEIVE_TIMEOUT_SEC * 1000) <= 0 ||
event.type != ENET_EVENT_TYPE_RECEIVE) {
Limelog("Failed to receive RTSP reply payload: %d\n", LastSocketFail());
goto Exit;
}
responseBuffer = extendBuffer(responseBuffer, event.packet->dataLength + offset);
if (responseBuffer == NULL) {
Limelog("Failed to extend RTSP response buffer\n");
enet_packet_destroy(event.packet);
goto Exit;
}
// Copy the payload out to the end of the response buffer and destroy the packet
memcpy(&responseBuffer[offset], event.packet->data, event.packet->dataLength);
offset += (int) event.packet->dataLength;
enet_packet_destroy(event.packet);
}
if (parseRtspMessage(response, responseBuffer, offset) == RTSP_ERROR_SUCCESS) {
// Successfully parsed response
ret = true;
}
else {
Limelog("Failed to parse RTSP response\n");
}
Exit:
// Swap back the payload pointer to avoid leaking memory later
request->payload = payload;
request->payloadLength = payloadLength;
// Free the serialized buffer
if (serializedMessage != NULL) {
free(serializedMessage);
}
// Free the response buffer
if (responseBuffer != NULL) {
free(responseBuffer);
}
return ret;
}
// Send RTSP message and get response over TCP
static bool transactRtspMessageTcp(PRTSP_MESSAGE request, PRTSP_MESSAGE response, int* error) {
SOCK_RET err;
bool ret;
int offset;
char* serializedMessage = NULL;
int messageLen;
char* responseBuffer;
int responseBufferSize;
int connectRetries;
*error = -1;
ret = false;
responseBuffer = NULL;
connectRetries = 0;
// Retry up to 10 seconds if we receive ECONNREFUSED errors from the host PC.
// This can happen with GFE 3.22 when initially launching a session because it
// returns HTTP 200 OK for the /launch request before the RTSP handshake port
// is listening.
do {
sock = connectTcpSocket(&RemoteAddr, AddrLen, RtspPortNumber, RTSP_CONNECT_TIMEOUT_SEC);
if (sock == INVALID_SOCKET) {
*error = LastSocketError();
if (*error == ECONNREFUSED) {
// Try again after 500 ms on ECONNREFUSED
PltSleepMs(RTSP_RETRY_DELAY_MS);
}
else {
// Fail if we get some other error
break;
}
}
else {
// We successfully connected
break;
}
} while (connectRetries++ < (RTSP_CONNECT_TIMEOUT_SEC * 1000) / RTSP_RETRY_DELAY_MS && !ConnectionInterrupted);
if (sock == INVALID_SOCKET) {
return ret;
}
serializedMessage = sealRtspMessage(request, &messageLen);
if (serializedMessage == NULL) {
closeSocket(sock);
sock = INVALID_SOCKET;
return ret;
}
// Send our message split into smaller chunks to avoid MTU issues.
// enableNoDelay() must have been called for sendMtuSafe() to work.
enableNoDelay(sock);
err = sendMtuSafe(sock, serializedMessage, messageLen);
if (err == SOCKET_ERROR) {
*error = LastSocketError();
Limelog("Failed to send RTSP message: %d\n", *error);
goto Exit;
}
// Read the response until the server closes the connection
offset = 0;
responseBufferSize = 0;
for (;;) {
struct pollfd pfd;
if (offset >= responseBufferSize) {
responseBufferSize = offset + 16384;
responseBuffer = extendBuffer(responseBuffer, responseBufferSize);
if (responseBuffer == NULL) {
Limelog("Failed to allocate RTSP response buffer\n");
goto Exit;
}
}
pfd.fd = sock;
pfd.events = POLLIN;
err = pollSockets(&pfd, 1, RTSP_RECEIVE_TIMEOUT_SEC * 1000);
if (err == 0) {
*error = ETIMEDOUT;
Limelog("RTSP request timed out\n");
goto Exit;
}
else if (err < 0) {
*error = LastSocketError();
Limelog("Failed to wait for RTSP response: %d\n", *error);
goto Exit;
}
err = recv(sock, &responseBuffer[offset], responseBufferSize - offset, 0);
if (err < 0) {
// Error reading
*error = LastSocketError();
Limelog("Failed to read RTSP response: %d\n", *error);
goto Exit;
}
else if (err == 0) {
// Done reading
break;
}
else {
offset += err;
}
}
// Decrypt (if necessary) and deserialize the RTSP response
ret = unsealRtspMessage(responseBuffer, offset, response);
// Fetch the local address for this socket if it's not populated yet
if (LocalAddr.ss_family == 0) {
SOCKADDR_LEN addrLen = (SOCKADDR_LEN)sizeof(LocalAddr);
if (getsockname(sock, (struct sockaddr*)&LocalAddr, &addrLen) < 0) {
Limelog("Failed to get local address: %d\n", LastSocketError());
memset(&LocalAddr, 0, sizeof(LocalAddr));
}
else {
LC_ASSERT(addrLen == AddrLen);
}
}
Exit:
if (serializedMessage != NULL) {
free(serializedMessage);
}
if (responseBuffer != NULL) {
free(responseBuffer);
}
closeSocket(sock);
sock = INVALID_SOCKET;
return ret;
}
static bool transactRtspMessage(PRTSP_MESSAGE request, PRTSP_MESSAGE response, bool expectingPayload, int* error) {
if (ConnectionInterrupted) {
*error = -1;
return false;
}
if (useEnet) {
return transactRtspMessageEnet(request, response, expectingPayload, error);
}
else {
return transactRtspMessageTcp(request, response, error);
}
}
// Send RTSP OPTIONS request
static bool requestOptions(PRTSP_MESSAGE response, int* error) {
RTSP_MESSAGE request;
bool ret;
*error = -1;
ret = initializeRtspRequest(&request, "OPTIONS", rtspTargetUrl);
if (ret) {
ret = transactRtspMessage(&request, response, false, error);
freeMessage(&request);
}
return ret;
}
// Send RTSP DESCRIBE request
static bool requestDescribe(PRTSP_MESSAGE response, int* error) {
RTSP_MESSAGE request;
bool ret;
*error = -1;
ret = initializeRtspRequest(&request, "DESCRIBE", rtspTargetUrl);
if (ret) {
if (addOption(&request, "Accept",
"application/sdp") &&
addOption(&request, "If-Modified-Since",
"Thu, 01 Jan 1970 00:00:00 GMT")) {
ret = transactRtspMessage(&request, response, true, error);
}
else {
ret = false;
}
freeMessage(&request);
}
return ret;
}
// Send RTSP SETUP request
static bool setupStream(PRTSP_MESSAGE response, char* target, int* error) {
RTSP_MESSAGE request;
bool ret;
char* transportValue;
*error = -1;
ret = initializeRtspRequest(&request, "SETUP", target);
if (ret) {
if (hasSessionId) {
if (!addOption(&request, "Session", sessionIdString)) {
ret = false;
goto FreeMessage;
}
}
if (AppVersionQuad[0] >= 6) {
// It looks like GFE doesn't care what we say our port is but
// we need to give it some port to successfully complete the
// handshake process.
transportValue = "unicast;X-GS-ClientPort=50000-50001";
}
else {
transportValue = " ";
}
if (addOption(&request, "Transport", transportValue) &&
addOption(&request, "If-Modified-Since",
"Thu, 01 Jan 1970 00:00:00 GMT")) {
ret = transactRtspMessage(&request, response, false, error);
}
else {
ret = false;
}
FreeMessage:
freeMessage(&request);
}
return ret;
}
// Send RTSP PLAY request
static bool playStream(PRTSP_MESSAGE response, char* target, int* error) {
RTSP_MESSAGE request;
bool ret;
*error = -1;
ret = initializeRtspRequest(&request, "PLAY", target);
if (ret != 0) {
if (addOption(&request, "Session", sessionIdString)) {
ret = transactRtspMessage(&request, response, false, error);
}
else {
ret = false;
}
freeMessage(&request);
}
return ret;
}
// Send RTSP ANNOUNCE message
static bool sendVideoAnnounce(PRTSP_MESSAGE response, int* error) {
RTSP_MESSAGE request;
bool ret;
int payloadLength;
char payloadLengthStr[16];
*error = -1;
ret = initializeRtspRequest(&request, "ANNOUNCE",
APP_VERSION_AT_LEAST(7, 1, 431) ? controlStreamId : "streamid=video");
if (ret) {
ret = false;
if (!addOption(&request, "Session", sessionIdString) ||
!addOption(&request, "Content-type", "application/sdp")) {
goto FreeMessage;
}
request.payload = getSdpPayloadForStreamConfig(rtspClientVersion, &payloadLength);
if (request.payload == NULL) {
goto FreeMessage;
}
request.flags |= FLAG_ALLOCATED_PAYLOAD;
request.payloadLength = payloadLength;
snprintf(payloadLengthStr, sizeof(payloadLengthStr), "%d", payloadLength);
if (!addOption(&request, "Content-length", payloadLengthStr)) {
goto FreeMessage;
}
ret = transactRtspMessage(&request, response, false, error);
FreeMessage:
freeMessage(&request);
}
return ret;
}
static int parseOpusConfigFromParamString(char* paramStr, int channelCount, POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
int i;
// Set channel count (included in the prefix, so not parsed below)
opusConfig->channelCount = channelCount;
// Parse the remaining data from the surround-params value
if (!CHAR_IS_DIGIT(*paramStr)) {
Limelog("Invalid stream count: %c\n", *paramStr);
return -1;
}
opusConfig->streams = CHAR_TO_INT(*paramStr);
paramStr++;
if (!CHAR_IS_DIGIT(*paramStr)) {
Limelog("Invalid coupled stream count: %c\n", *paramStr);
return -2;
}
opusConfig->coupledStreams = CHAR_TO_INT(*paramStr);
paramStr++;
for (i = 0; i < opusConfig->channelCount; i++) {
if (!CHAR_IS_DIGIT(*paramStr)) {
Limelog("Invalid mapping value at %d: %c\n", i, *paramStr);
return -3;
}
opusConfig->mapping[i] = CHAR_TO_INT(*paramStr);
paramStr++;
}
return 0;
}
// Parse the server port from the Transport header
// Example: unicast;server_port=48000-48001;source=192.168.35.177
static bool parseServerPortFromTransport(PRTSP_MESSAGE response, uint16_t* port) {
char* transport;
char* portStart;
transport = getOptionContent(response->options, "Transport");
if (transport == NULL) {
return false;
}
// Look for the server_port= entry in the Transport option
portStart = strstr(transport, "server_port=");
if (portStart == NULL) {
return false;
}
// Skip the prefix
portStart += strlen("server_port=");
// Validate the port number
long int rawPort = strtol(portStart, NULL, 10);
if (rawPort <= 0 || rawPort > 65535) {
return false;
}
*port = (uint16_t)rawPort;
return true;
}
// Parses the Opus configuration from an RTSP DESCRIBE response
static int parseOpusConfigurations(PRTSP_MESSAGE response) {
HighQualitySurroundSupported = false;
memset(&NormalQualityOpusConfig, 0, sizeof(NormalQualityOpusConfig));
memset(&HighQualityOpusConfig, 0, sizeof(HighQualityOpusConfig));
// Sample rate is always 48 KHz
HighQualityOpusConfig.sampleRate = NormalQualityOpusConfig.sampleRate = 48000;
// Stereo doesn't have any surround-params elements in the RTSP data
if (CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration) == 2) {
NormalQualityOpusConfig.channelCount = 2;
NormalQualityOpusConfig.streams = 1;
NormalQualityOpusConfig.coupledStreams = 1;
NormalQualityOpusConfig.mapping[0] = 0;
NormalQualityOpusConfig.mapping[1] = 1;
}
else {
char paramsPrefix[128];
char* paramStart;
int err;
int channelCount;
channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration);
// Find the correct audio parameter value
snprintf(paramsPrefix, sizeof(paramsPrefix), "a=fmtp:97 surround-params=%d", channelCount);
paramStart = strstr(response->payload, paramsPrefix);
if (paramStart) {
// Skip the prefix
paramStart += strlen(paramsPrefix);
// Parse the normal quality Opus config
err = parseOpusConfigFromParamString(paramStart, channelCount, &NormalQualityOpusConfig);
if (err != 0) {
return err;
}
// GFE's normal-quality channel mapping differs from the one our clients use.
// They use FL FR C RL RR SL SR LFE, but we use FL FR C LFE RL RR SL SR. We'll need
// to swap the mappings to match the expected values.
if (channelCount == 6 || channelCount == 8) {
OPUS_MULTISTREAM_CONFIGURATION originalMapping = NormalQualityOpusConfig;
// LFE comes after C
NormalQualityOpusConfig.mapping[3] = originalMapping.mapping[channelCount - 1];
// Slide everything else up
memcpy(&NormalQualityOpusConfig.mapping[4],
&originalMapping.mapping[3],
channelCount - 4);
}
// If this configuration is compatible with high quality mode, we may have another
// matching surround-params value for high quality mode.
paramStart = strstr(paramStart, paramsPrefix);
if (paramStart) {
// Skip the prefix
paramStart += strlen(paramsPrefix);
// Parse the high quality Opus config
err = parseOpusConfigFromParamString(paramStart, channelCount, &HighQualityOpusConfig);
if (err != 0) {
return err;
}
// We can request high quality audio
HighQualitySurroundSupported = true;
}
}
else {
Limelog("No surround parameters found for channel count: %d\n", channelCount);
// It's unknown whether all GFE versions that supported surround sound included these
// surround sound parameters. In case they didn't, we'll specifically handle 5.1 surround
// sound using a hardcoded configuration like we used to before this parsing code existed.
//
// It is not necessary to provide HighQualityOpusConfig here because high quality mode
// can only be enabled from seeing the required "surround-params=" value, so there's no
// chance it could regress from implementing this parser.
if (channelCount == 6) {
NormalQualityOpusConfig.channelCount = 6;
NormalQualityOpusConfig.streams = 4;
NormalQualityOpusConfig.coupledStreams = 2;
NormalQualityOpusConfig.mapping[0] = 0;
NormalQualityOpusConfig.mapping[1] = 4;
NormalQualityOpusConfig.mapping[2] = 1;
NormalQualityOpusConfig.mapping[3] = 5;
NormalQualityOpusConfig.mapping[4] = 2;
NormalQualityOpusConfig.mapping[5] = 3;
}
else {
// We don't have a hardcoded fallback mapping, so we have no choice but to fail.
return -4;
}
}
}
return 0;
}
static bool parseUrlAddrFromRtspUrlString(const char* rtspUrlString, char* destination, size_t destinationLength) {
char* rtspUrlScratchBuffer;
char* portSeparator;
char* v6EscapeEndChar;
char* urlPathSeparator;
int prefixLen;
// Create a copy that we can modify
rtspUrlScratchBuffer = strdup(rtspUrlString);
if (rtspUrlScratchBuffer == NULL) {
return false;
}
// If we have a v6 address, we want to stop one character after the closing ]
// If we have a v4 address, we want to stop at the port separator
portSeparator = strrchr(rtspUrlScratchBuffer, ':');
v6EscapeEndChar = strchr(rtspUrlScratchBuffer, ']');
// Count the prefix length to skip past the initial rtsp:// or rtspru:// part
for (prefixLen = 2; rtspUrlScratchBuffer[prefixLen - 2] != 0 && (rtspUrlScratchBuffer[prefixLen - 2] != '/' || rtspUrlScratchBuffer[prefixLen - 1] != '/'); prefixLen++);
// If we hit the end of the string prior to parsing the prefix, we cannot proceed
if (rtspUrlScratchBuffer[prefixLen - 2] == 0) {
free(rtspUrlScratchBuffer);
return false;
}
// Look for a slash at the end of the host portion of the URL (may not be present)
urlPathSeparator = strchr(rtspUrlScratchBuffer + prefixLen, '/');
// Check for a v6 address first since they also have colons
if (v6EscapeEndChar) {
// Terminate the string at the next character
*(v6EscapeEndChar + 1) = 0;
}
else if (portSeparator) {
// Terminate the string prior to the port separator
*portSeparator = 0;
}
else if (urlPathSeparator) {
// Terminate the string prior to the path separator
*urlPathSeparator = 0;
}
if (!PltSafeStrcpy(destination, destinationLength, rtspUrlScratchBuffer + prefixLen)) {
free(rtspUrlScratchBuffer);
return false;
}
free(rtspUrlScratchBuffer);
return true;
}
// SDP attributes are in the form:
// a=x-nv-bwe.bwuSafeZoneLowLimit:70\r\n
bool parseSdpAttributeToUInt(const char* payload, const char* name, unsigned int* val) {
// Find the entry for the specified attribute name
char* attribute = strstr(payload, name);
if (!attribute) {
return false;
}
// Locate the start of the value
char* valst = strstr(attribute, ":");
if (!valst) {
return false;
}
// Read the integer up to the newline at the end of the SDP attribute
*val = strtoul(valst + 1, NULL, 0);
return true;
}
bool parseSdpAttributeToInt(const char* payload, const char* name, int* val) {
// Find the entry for the specified attribute name
char* attribute = strstr(payload, name);
if (!attribute) {
return false;
}
// Locate the start of the value
char* valst = strstr(attribute, ":");
if (!valst) {
return false;
}
// Read the integer up to the newline at the end of the SDP attribute
*val = strtol(valst + 1, NULL, 0);
return true;
}
// Perform RTSP Handshake with the streaming server machine as part of the connection process
int performRtspHandshake(PSERVER_INFORMATION serverInfo) {
int ret;
LC_ASSERT(RtspPortNumber != 0);
// Initialize global state
useEnet = (AppVersionQuad[0] >= 5) && (AppVersionQuad[0] <= 7) && (AppVersionQuad[2] < 404);
currentSeqNumber = 1;
hasSessionId = false;
controlStreamId = APP_VERSION_AT_LEAST(7, 1, 431) ? "streamid=control/13/0" : "streamid=control/1/0";
AudioEncryptionEnabled = false;
encryptedRtspEnabled = serverInfo->rtspSessionUrl && strstr(serverInfo->rtspSessionUrl, "rtspenc://");
encryptionCtx = PltCreateCryptoContext();
decryptionCtx = PltCreateCryptoContext();
// HACK: In order to get GFE to respect our request for a lower audio bitrate, we must
// fake our target address so it doesn't match any of the PC's local interfaces. It seems
// that the only way to get it to give you "low quality" stereo audio nowadays is if it
// thinks you are remote (target address != any local address).
//
// We will enable high quality audio if the following are all true:
// 1. Video bitrate is higher than 15 Mbps (to ensure most bandwidth is reserved for video)
// 2. The audio decoder has not declared that it is slow
// 3. The stream is either local or not surround sound (to prevent MTU issues over the Internet)
LC_ASSERT(StreamConfig.streamingRemotely != STREAM_CFG_AUTO);
if (StreamConfig.bitrate >= HIGH_AUDIO_BITRATE_THRESHOLD &&
(AudioCallbacks.capabilities & CAPABILITY_SLOW_OPUS_DECODER) == 0 &&
(StreamConfig.streamingRemotely != STREAM_CFG_REMOTE || CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration) <= 2)) {
// If we have an RTSP URL string and it was successfully parsed and copied, use that string
if (serverInfo->rtspSessionUrl == NULL ||
!parseUrlAddrFromRtspUrlString(serverInfo->rtspSessionUrl, urlAddr, sizeof(urlAddr)) ||
!PltSafeStrcpy(rtspTargetUrl, sizeof(rtspTargetUrl), serverInfo->rtspSessionUrl)) {
// If an RTSP URL string was not provided or failed to parse, we will construct one now as best we can.
//
// NB: If the remote address is not a LAN address, the host will likely not enable high quality
// audio since it only does that for local streaming normally. We can avoid this limitation,
// but only if the caller gave us the RTSP session URL that it received from the host during launch.
addrToUrlSafeString(&RemoteAddr, urlAddr, sizeof(urlAddr));
snprintf(rtspTargetUrl, sizeof(rtspTargetUrl), "rtsp%s://%s:%u", useEnet ? "ru" : "", urlAddr, RtspPortNumber);
}
}
else {
PltSafeStrcpy(urlAddr, sizeof(urlAddr), "0.0.0.0");
snprintf(rtspTargetUrl, sizeof(rtspTargetUrl), "rtsp%s://%s:%u", useEnet ? "ru" : "", urlAddr, RtspPortNumber);
}
switch (AppVersionQuad[0]) {
case 3:
rtspClientVersion = 10;
break;
case 4:
rtspClientVersion = 11;
break;
case 5:
rtspClientVersion = 12;
break;
case 6:
// Gen 6 has never been seen in the wild
rtspClientVersion = 13;
break;
case 7:
default:
rtspClientVersion = 14;
break;
}
// Setup ENet if required by this GFE version
if (useEnet) {
ENetAddress address;
ENetEvent event;
enet_address_set_address(&address, (struct sockaddr *)&RemoteAddr, AddrLen);
enet_address_set_port(&address, RtspPortNumber);
// Create a client that can use 1 outgoing connection and 1 channel
client = enet_host_create(RemoteAddr.ss_family, NULL, 1, 1, 0, 0);
if (client == NULL) {
return -1;
}
// Connect to the host
peer = enet_host_connect(client, &address, 1, 0);
if (peer == NULL) {
enet_host_destroy(client);
client = NULL;
return -1;
}
// Wait for the connect to complete
if (serviceEnetHost(client, &event, RTSP_CONNECT_TIMEOUT_SEC * 1000) <= 0 ||
event.type != ENET_EVENT_TYPE_CONNECT) {
Limelog("RTSP: Failed to connect to UDP port %u: error %d\n", RtspPortNumber, LastSocketFail());
enet_peer_reset(peer);
peer = NULL;
enet_host_destroy(client);
client = NULL;
return -1;
}
// Ensure the connect verify ACK is sent immediately
enet_host_flush(client);
}
{
RTSP_MESSAGE response;
int error = -1;
if (!requestOptions(&response, &error)) {
Limelog("RTSP OPTIONS request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP OPTIONS request failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
freeMessage(&response);
}
{
RTSP_MESSAGE response;
int error = -1;
if (!requestDescribe(&response, &error)) {
Limelog("RTSP DESCRIBE request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP DESCRIBE request failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
if ((StreamConfig.supportedVideoFormats & VIDEO_FORMAT_MASK_AV1) && strstr(response.payload, "AV1/90000")) {
if ((serverInfo->serverCodecModeSupport & SCM_AV1_MAIN10) && (StreamConfig.supportedVideoFormats & VIDEO_FORMAT_AV1_MAIN10)) {
NegotiatedVideoFormat = VIDEO_FORMAT_AV1_MAIN10;
}
else {
NegotiatedVideoFormat = VIDEO_FORMAT_AV1_MAIN8;
}
}
else if ((StreamConfig.supportedVideoFormats & VIDEO_FORMAT_MASK_H265) && strstr(response.payload, "sprop-parameter-sets=AAAAAU")) {
// The RTSP DESCRIBE reply will contain a collection of SDP media attributes that
// describe the various supported video stream formats and include the SPS, PPS,
// and VPS (if applicable). We will use this information to determine whether the
// server can support HEVC. For some reason, they still set the MIME type of the HEVC
// format to H264, so we can't just look for the HEVC MIME type. What we'll do instead is
// look for the base 64 encoded VPS NALU prefix that is unique to the HEVC bitstream.
if ((serverInfo->serverCodecModeSupport & SCM_HEVC_MAIN10) && (StreamConfig.supportedVideoFormats & VIDEO_FORMAT_H265_MAIN10)) {
NegotiatedVideoFormat = VIDEO_FORMAT_H265_MAIN10;
}
else {
NegotiatedVideoFormat = VIDEO_FORMAT_H265;
}
}
else {
NegotiatedVideoFormat = VIDEO_FORMAT_H264;
// Dimensions over 4096 are only supported with HEVC on NVENC
if (StreamConfig.width > 4096 || StreamConfig.height > 4096) {
Limelog("WARNING: Host PC doesn't support HEVC. Streaming at resolutions above 4K using H.264 will likely fail!\n");
}
}
// Look for the SDP attribute that indicates we're dealing with a server that supports RFI
ReferenceFrameInvalidationSupported = strstr(response.payload, "x-nv-video[0].refPicInvalidation") != NULL;
if (!ReferenceFrameInvalidationSupported) {
Limelog("Reference frame invalidation is not supported by this host\n");
}
// Look for the Sunshine feature flags in the SDP attributes
if (!parseSdpAttributeToUInt(response.payload, "x-ss-general.featureFlags", &SunshineFeatureFlags)) {
SunshineFeatureFlags = 0;
}
// Look for the Sunshine encryption flags in the SDP attributes
if (!parseSdpAttributeToUInt(response.payload, "x-ss-general.encryptionSupported", &EncryptionFeaturesSupported)) {
EncryptionFeaturesSupported = 0;
}
if (!parseSdpAttributeToUInt(response.payload, "x-ss-general.encryptionRequested", &EncryptionFeaturesRequested)) {
EncryptionFeaturesRequested = 0;
}
EncryptionFeaturesEnabled = 0;
// Parse the Opus surround parameters out of the RTSP DESCRIBE response.
ret = parseOpusConfigurations(&response);
if (ret != 0) {
goto Exit;
}
freeMessage(&response);
}
{
RTSP_MESSAGE response;
char* sessionId;
char* pingPayload;
int error = -1;
char* strtokCtx = NULL;
if (!setupStream(&response,
AppVersionQuad[0] >= 5 ? "streamid=audio/0/0" : "streamid=audio",
&error)) {
Limelog("RTSP SETUP streamid=audio request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP SETUP streamid=audio request failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
// Parse the audio port out of the RTSP SETUP response
LC_ASSERT(AudioPortNumber == 0);
if (!parseServerPortFromTransport(&response, &AudioPortNumber)) {
// Use the well known port if parsing fails
AudioPortNumber = 48000;
Limelog("Audio port: %u (RTSP parsing failed)\n", AudioPortNumber);
}
else {
Limelog("Audio port: %u\n", AudioPortNumber);
}
// Parse the Sunshine ping payload protocol extension if present
memset(&AudioPingPayload, 0, sizeof(AudioPingPayload));
pingPayload = getOptionContent(response.options, "X-SS-Ping-Payload");
if (pingPayload != NULL && strlen(pingPayload) == sizeof(AudioPingPayload.payload)) {
memcpy(AudioPingPayload.payload, pingPayload, sizeof(AudioPingPayload.payload));
}
// Let the audio stream know the port number is now finalized.
// NB: This is needed because audio stream init happens before RTSP,
// which is not the case for the video stream.
notifyAudioPortNegotiationComplete();
sessionId = getOptionContent(response.options, "Session");
if (sessionId == NULL) {
Limelog("RTSP SETUP streamid=audio is missing session attribute\n");
ret = -1;
goto Exit;
}
// Given there is a non-null session id, get the
// first token of the session until ";", which
// resolves any 454 session not found errors on
// standard RTSP server implementations.
// (i.e - sessionId = "DEADBEEFCAFE;timeout = 90")
sessionIdString = strdup(strtok_r(sessionId, ";", &strtokCtx));
if (sessionIdString == NULL) {
Limelog("Failed to duplicate session ID string\n");
ret = -1;
goto Exit;
}
hasSessionId = true;
freeMessage(&response);
}
{
RTSP_MESSAGE response;
int error = -1;
char* pingPayload;
if (!setupStream(&response,
AppVersionQuad[0] >= 5 ? "streamid=video/0/0" : "streamid=video",
&error)) {
Limelog("RTSP SETUP streamid=video request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP SETUP streamid=video request failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
// Parse the Sunshine ping payload protocol extension if present
memset(&VideoPingPayload, 0, sizeof(VideoPingPayload));
pingPayload = getOptionContent(response.options, "X-SS-Ping-Payload");
if (pingPayload != NULL && strlen(pingPayload) == sizeof(VideoPingPayload.payload)) {
memcpy(VideoPingPayload.payload, pingPayload, sizeof(VideoPingPayload.payload));
}
// Parse the video port out of the RTSP SETUP response
LC_ASSERT(VideoPortNumber == 0);
if (!parseServerPortFromTransport(&response, &VideoPortNumber)) {
// Use the well known port if parsing fails
VideoPortNumber = 47998;
Limelog("Video port: %u (RTSP parsing failed)\n", VideoPortNumber);
}
else {
Limelog("Video port: %u\n", VideoPortNumber);
}
freeMessage(&response);
}
if (AppVersionQuad[0] >= 5) {
RTSP_MESSAGE response;
int error = -1;
char* connectData;
if (!setupStream(&response,
controlStreamId,
&error)) {
Limelog("RTSP SETUP streamid=control request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP SETUP streamid=control request failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
// Parse the Sunshine control connect data extension if present
connectData = getOptionContent(response.options, "X-SS-Connect-Data");
if (connectData != NULL) {
ControlConnectData = (uint32_t)strtoul(connectData, NULL, 0);
}
else {
ControlConnectData = 0;
}
// Parse the control port out of the RTSP SETUP response
LC_ASSERT(ControlPortNumber == 0);
if (!parseServerPortFromTransport(&response, &ControlPortNumber)) {
// Use the well known port if parsing fails
ControlPortNumber = 47999;
Limelog("Control port: %u (RTSP parsing failed)\n", ControlPortNumber);
}
else {
Limelog("Control port: %u\n", ControlPortNumber);
}
freeMessage(&response);
}
{
RTSP_MESSAGE response;
int error = -1;
if (!sendVideoAnnounce(&response, &error)) {
Limelog("RTSP ANNOUNCE request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP ANNOUNCE request failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
freeMessage(&response);
}
// GFE 3.22 uses a single PLAY message
if (APP_VERSION_AT_LEAST(7, 1, 431)) {
RTSP_MESSAGE response;
int error = -1;
if (!playStream(&response, "/", &error)) {
Limelog("RTSP PLAY request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP PLAY failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
freeMessage(&response);
}
else {
{
RTSP_MESSAGE response;
int error = -1;
if (!playStream(&response, "streamid=video", &error)) {
Limelog("RTSP PLAY streamid=video request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP PLAY streamid=video failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
freeMessage(&response);
}
{
RTSP_MESSAGE response;
int error = -1;
if (!playStream(&response, "streamid=audio", &error)) {
Limelog("RTSP PLAY streamid=audio request failed: %d\n", error);
ret = error;
goto Exit;
}
if (response.message.response.statusCode != 200) {
Limelog("RTSP PLAY streamid=audio failed: %d\n",
response.message.response.statusCode);
ret = response.message.response.statusCode;
goto Exit;
}
freeMessage(&response);
}
}
ret = 0;
Exit:
// Cleanup the ENet stuff
if (useEnet) {
if (peer != NULL) {
enet_peer_disconnect_now(peer, 0);
peer = NULL;
}
if (client != NULL) {
enet_host_destroy(client);
client = NULL;
}
}
if (sessionIdString != NULL) {
free(sessionIdString);
sessionIdString = NULL;
}
PltDestroyCryptoContext(encryptionCtx);
PltDestroyCryptoContext(decryptionCtx);
decryptionCtx = encryptionCtx = NULL;
return ret;
}