mirror of
https://github.com/moonlight-stream/moonlight-common-c.git
synced 2025-07-01 23:35:58 +00:00
1365 lines
45 KiB
C
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;
|
|
}
|