mirror of
https://github.com/moonlight-stream/moonlight-common-c.git
synced 2025-08-17 17:05:50 +00:00
1339 lines
46 KiB
C
1339 lines
46 KiB
C
#include "Limelight-internal.h"
|
|
|
|
// NV control stream packet header for TCP
|
|
typedef struct _NVCTL_TCP_PACKET_HEADER {
|
|
unsigned short type;
|
|
unsigned short payloadLength;
|
|
} NVCTL_TCP_PACKET_HEADER, *PNVCTL_TCP_PACKET_HEADER;
|
|
|
|
typedef struct _NVCTL_ENET_PACKET_HEADER_V1 {
|
|
unsigned short type;
|
|
} NVCTL_ENET_PACKET_HEADER_V1, *PNVCTL_ENET_PACKET_HEADER_V1;
|
|
|
|
typedef struct _NVCTL_ENET_PACKET_HEADER_V2 {
|
|
unsigned short type;
|
|
unsigned short payloadLength;
|
|
} NVCTL_ENET_PACKET_HEADER_V2, *PNVCTL_ENET_PACKET_HEADER_V2;
|
|
|
|
#define AES_GCM_TAG_LENGTH 16
|
|
typedef struct _NVCTL_ENCRYPTED_PACKET_HEADER {
|
|
unsigned short encryptedHeaderType; // Always LE 0x0001
|
|
unsigned short length; // sizeof(seq) + 16 byte tag + secondary header and data
|
|
unsigned int seq; // Monotonically increasing sequence number (used as IV for AES-GCM)
|
|
|
|
// encrypted NVCTL_ENET_PACKET_HEADER_V2 and payload data follow
|
|
} NVCTL_ENCRYPTED_PACKET_HEADER, *PNVCTL_ENCRYPTED_PACKET_HEADER;
|
|
|
|
typedef struct _QUEUED_FRAME_INVALIDATION_TUPLE {
|
|
int startFrame;
|
|
int endFrame;
|
|
LINKED_BLOCKING_QUEUE_ENTRY entry;
|
|
} QUEUED_FRAME_INVALIDATION_TUPLE, *PQUEUED_FRAME_INVALIDATION_TUPLE;
|
|
|
|
static SOCKET ctlSock = INVALID_SOCKET;
|
|
static ENetHost* client;
|
|
static ENetPeer* peer;
|
|
static PLT_MUTEX enetMutex;
|
|
static bool usePeriodicPing;
|
|
|
|
static PLT_THREAD lossStatsThread;
|
|
static PLT_THREAD invalidateRefFramesThread;
|
|
static PLT_THREAD requestIdrFrameThread;
|
|
static PLT_THREAD controlReceiveThread;
|
|
static int lossCountSinceLastReport;
|
|
static int lastGoodFrame;
|
|
static int lastSeenFrame;
|
|
static bool stopping;
|
|
static bool disconnectPending;
|
|
static bool encryptedControlStream;
|
|
|
|
static int intervalGoodFrameCount;
|
|
static int intervalTotalFrameCount;
|
|
static uint64_t intervalStartTimeMs;
|
|
static int lastIntervalLossPercentage;
|
|
static int lastConnectionStatusUpdate;
|
|
static int currentEnetSequenceNumber;
|
|
|
|
static LINKED_BLOCKING_QUEUE invalidReferenceFrameTuples;
|
|
static PLT_EVENT idrFrameRequiredEvent;
|
|
|
|
static PPLT_CRYPTO_CONTEXT encryptionCtx;
|
|
static PPLT_CRYPTO_CONTEXT decryptionCtx;
|
|
|
|
#define CONN_IMMEDIATE_POOR_LOSS_RATE 30
|
|
#define CONN_CONSECUTIVE_POOR_LOSS_RATE 15
|
|
#define CONN_OKAY_LOSS_RATE 5
|
|
#define CONN_STATUS_SAMPLE_PERIOD 3000
|
|
|
|
#define IDX_START_A 0
|
|
#define IDX_REQUEST_IDR_FRAME 0
|
|
#define IDX_START_B 1
|
|
#define IDX_INVALIDATE_REF_FRAMES 2
|
|
#define IDX_LOSS_STATS 3
|
|
#define IDX_INPUT_DATA 5
|
|
#define IDX_RUMBLE_DATA 6
|
|
#define IDX_TERMINATION 7
|
|
|
|
#define CONTROL_STREAM_TIMEOUT_SEC 10
|
|
#define CONTROL_STREAM_LINGER_TIMEOUT_SEC 2
|
|
|
|
static const short packetTypesGen3[] = {
|
|
0x1407, // Request IDR frame
|
|
0x1410, // Start B
|
|
0x1404, // Invalidate reference frames
|
|
0x140c, // Loss Stats
|
|
0x1417, // Frame Stats (unused)
|
|
-1, // Input data (unused)
|
|
-1, // Rumble data (unused)
|
|
-1, // Termination (unused)
|
|
};
|
|
static const short packetTypesGen4[] = {
|
|
0x0606, // Request IDR frame
|
|
0x0609, // Start B
|
|
0x0604, // Invalidate reference frames
|
|
0x060a, // Loss Stats
|
|
0x0611, // Frame Stats (unused)
|
|
-1, // Input data (unused)
|
|
-1, // Rumble data (unused)
|
|
-1, // Termination (unused)
|
|
};
|
|
static const short packetTypesGen5[] = {
|
|
0x0305, // Start A
|
|
0x0307, // Start B
|
|
0x0301, // Invalidate reference frames
|
|
0x0201, // Loss Stats
|
|
0x0204, // Frame Stats (unused)
|
|
0x0207, // Input data
|
|
-1, // Rumble data (unused)
|
|
-1, // Termination (unused)
|
|
};
|
|
static const short packetTypesGen7[] = {
|
|
0x0305, // Start A
|
|
0x0307, // Start B
|
|
0x0301, // Invalidate reference frames
|
|
0x0201, // Loss Stats
|
|
0x0204, // Frame Stats (unused)
|
|
0x0206, // Input data
|
|
0x010b, // Rumble data
|
|
0x0100, // Termination
|
|
};
|
|
static const short packetTypesGen7Enc[] = {
|
|
0x0302, // Request IDR frame
|
|
0x0307, // Start B
|
|
0x0301, // Invalidate reference frames
|
|
0x0201, // Loss Stats
|
|
0x0204, // Frame Stats (unused)
|
|
0x0206, // Input data
|
|
0x010b, // Rumble data
|
|
0x0109, // Termination (extended)
|
|
};
|
|
|
|
static const char requestIdrFrameGen3[] = { 0, 0 };
|
|
static const int startBGen3[] = { 0, 0, 0, 0xa };
|
|
|
|
static const char requestIdrFrameGen4[] = { 0, 0 };
|
|
static const char startBGen4[] = { 0 };
|
|
|
|
static const char startAGen5[] = { 0, 0 };
|
|
static const char startBGen5[] = { 0 };
|
|
|
|
static const char requestIdrFrameGen7Enc[] = { 0, 0 };
|
|
|
|
static const short payloadLengthsGen3[] = {
|
|
sizeof(requestIdrFrameGen3), // Request IDR frame
|
|
sizeof(startBGen3), // Start B
|
|
24, // Invalidate reference frames
|
|
32, // Loss Stats
|
|
64, // Frame Stats
|
|
-1, // Input data
|
|
};
|
|
static const short payloadLengthsGen4[] = {
|
|
sizeof(requestIdrFrameGen4), // Request IDR frame
|
|
sizeof(startBGen4), // Start B
|
|
24, // Invalidate reference frames
|
|
32, // Loss Stats
|
|
64, // Frame Stats
|
|
-1, // Input data
|
|
};
|
|
static const short payloadLengthsGen5[] = {
|
|
sizeof(startAGen5), // Start A
|
|
sizeof(startBGen5), // Start B
|
|
24, // Invalidate reference frames
|
|
32, // Loss Stats
|
|
80, // Frame Stats
|
|
-1, // Input data
|
|
};
|
|
static const short payloadLengthsGen7[] = {
|
|
sizeof(startAGen5), // Start A
|
|
sizeof(startBGen5), // Start B
|
|
24, // Invalidate reference frames
|
|
32, // Loss Stats
|
|
80, // Frame Stats
|
|
-1, // Input data
|
|
};
|
|
static const short payloadLengthsGen7Enc[] = {
|
|
sizeof(requestIdrFrameGen7Enc), // Request IDR frame
|
|
sizeof(startBGen5), // Start B
|
|
24, // Invalidate reference frames
|
|
32, // Loss Stats
|
|
80, // Frame Stats
|
|
-1, // Input data
|
|
};
|
|
|
|
static const char* preconstructedPayloadsGen3[] = {
|
|
requestIdrFrameGen3,
|
|
(char*)startBGen3
|
|
};
|
|
static const char* preconstructedPayloadsGen4[] = {
|
|
requestIdrFrameGen4,
|
|
startBGen4
|
|
};
|
|
static const char* preconstructedPayloadsGen5[] = {
|
|
startAGen5,
|
|
startBGen5
|
|
};
|
|
static const char* preconstructedPayloadsGen7[] = {
|
|
startAGen5,
|
|
startBGen5
|
|
};
|
|
static const char* preconstructedPayloadsGen7Enc[] = {
|
|
requestIdrFrameGen7Enc,
|
|
startBGen5
|
|
};
|
|
|
|
static short* packetTypes;
|
|
static short* payloadLengths;
|
|
static char**preconstructedPayloads;
|
|
static bool supportsIdrFrameRequest;
|
|
|
|
#define LOSS_REPORT_INTERVAL_MS 50
|
|
#define PERIODIC_PING_INTERVAL_MS 250
|
|
|
|
// Initializes the control stream
|
|
int initializeControlStream(void) {
|
|
stopping = false;
|
|
PltCreateEvent(&idrFrameRequiredEvent);
|
|
LbqInitializeLinkedBlockingQueue(&invalidReferenceFrameTuples, 20);
|
|
PltCreateMutex(&enetMutex);
|
|
|
|
encryptedControlStream = APP_VERSION_AT_LEAST(7, 1, 431);
|
|
|
|
if (AppVersionQuad[0] == 3) {
|
|
packetTypes = (short*)packetTypesGen3;
|
|
payloadLengths = (short*)payloadLengthsGen3;
|
|
preconstructedPayloads = (char**)preconstructedPayloadsGen3;
|
|
supportsIdrFrameRequest = true;
|
|
}
|
|
else if (AppVersionQuad[0] == 4) {
|
|
packetTypes = (short*)packetTypesGen4;
|
|
payloadLengths = (short*)payloadLengthsGen4;
|
|
preconstructedPayloads = (char**)preconstructedPayloadsGen4;
|
|
supportsIdrFrameRequest = true;
|
|
}
|
|
else if (AppVersionQuad[0] == 5) {
|
|
packetTypes = (short*)packetTypesGen5;
|
|
payloadLengths = (short*)payloadLengthsGen5;
|
|
preconstructedPayloads = (char**)preconstructedPayloadsGen5;
|
|
supportsIdrFrameRequest = false;
|
|
}
|
|
else {
|
|
if (encryptedControlStream) {
|
|
packetTypes = (short*)packetTypesGen7Enc;
|
|
payloadLengths = (short*)payloadLengthsGen7Enc;
|
|
preconstructedPayloads = (char**)preconstructedPayloadsGen7Enc;
|
|
supportsIdrFrameRequest = true;
|
|
}
|
|
else {
|
|
packetTypes = (short*)packetTypesGen7;
|
|
payloadLengths = (short*)payloadLengthsGen7;
|
|
preconstructedPayloads = (char**)preconstructedPayloadsGen7;
|
|
supportsIdrFrameRequest = false;
|
|
}
|
|
}
|
|
|
|
lastGoodFrame = 0;
|
|
lastSeenFrame = 0;
|
|
lossCountSinceLastReport = 0;
|
|
disconnectPending = false;
|
|
intervalGoodFrameCount = 0;
|
|
intervalTotalFrameCount = 0;
|
|
intervalStartTimeMs = 0;
|
|
lastIntervalLossPercentage = 0;
|
|
lastConnectionStatusUpdate = CONN_STATUS_OKAY;
|
|
currentEnetSequenceNumber = 0;
|
|
usePeriodicPing = APP_VERSION_AT_LEAST(7, 1, 415);
|
|
encryptionCtx = PltCreateCryptoContext();
|
|
decryptionCtx = PltCreateCryptoContext();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void freeFrameInvalidationList(PLINKED_BLOCKING_QUEUE_ENTRY entry) {
|
|
PLINKED_BLOCKING_QUEUE_ENTRY nextEntry;
|
|
|
|
while (entry != NULL) {
|
|
nextEntry = entry->flink;
|
|
free(entry->data);
|
|
entry = nextEntry;
|
|
}
|
|
}
|
|
|
|
// Cleans up control stream
|
|
void destroyControlStream(void) {
|
|
LC_ASSERT(stopping);
|
|
PltDestroyCryptoContext(encryptionCtx);
|
|
PltDestroyCryptoContext(decryptionCtx);
|
|
PltCloseEvent(&idrFrameRequiredEvent);
|
|
freeFrameInvalidationList(LbqDestroyLinkedBlockingQueue(&invalidReferenceFrameTuples));
|
|
PltDeleteMutex(&enetMutex);
|
|
}
|
|
|
|
void queueFrameInvalidationTuple(int startFrame, int endFrame) {
|
|
LC_ASSERT(startFrame <= endFrame);
|
|
|
|
if (isReferenceFrameInvalidationEnabled()) {
|
|
PQUEUED_FRAME_INVALIDATION_TUPLE qfit;
|
|
qfit = malloc(sizeof(*qfit));
|
|
if (qfit != NULL) {
|
|
qfit->startFrame = startFrame;
|
|
qfit->endFrame = endFrame;
|
|
if (LbqOfferQueueItem(&invalidReferenceFrameTuples, qfit, &qfit->entry) == LBQ_BOUND_EXCEEDED) {
|
|
// Too many invalidation tuples, so we need an IDR frame now
|
|
Limelog("RFI range list reached maximum size limit\n");
|
|
free(qfit);
|
|
requestIdrOnDemand();
|
|
}
|
|
}
|
|
else {
|
|
requestIdrOnDemand();
|
|
}
|
|
}
|
|
else {
|
|
requestIdrOnDemand();
|
|
}
|
|
}
|
|
|
|
// Request an IDR frame on demand by the decoder
|
|
void requestIdrOnDemand(void) {
|
|
// Any reference frame invalidation requests should be dropped now.
|
|
// We require a full IDR frame to recover.
|
|
freeFrameInvalidationList(LbqFlushQueueItems(&invalidReferenceFrameTuples));
|
|
|
|
// Request the IDR frame
|
|
PltSetEvent(&idrFrameRequiredEvent);
|
|
}
|
|
|
|
// Invalidate reference frames lost by the network
|
|
void connectionDetectedFrameLoss(int startFrame, int endFrame) {
|
|
queueFrameInvalidationTuple(startFrame, endFrame);
|
|
}
|
|
|
|
// When we receive a frame, update the number of our current frame
|
|
void connectionReceivedCompleteFrame(int frameIndex) {
|
|
lastGoodFrame = frameIndex;
|
|
intervalGoodFrameCount++;
|
|
}
|
|
|
|
void connectionSawFrame(int frameIndex) {
|
|
LC_ASSERT(!isBefore16(frameIndex, lastSeenFrame));
|
|
|
|
uint64_t now = PltGetMillis();
|
|
if (now - intervalStartTimeMs >= CONN_STATUS_SAMPLE_PERIOD) {
|
|
if (intervalTotalFrameCount != 0) {
|
|
// Notify the client of connection status changes based on frame loss rate
|
|
int frameLossPercent = 100 - (intervalGoodFrameCount * 100) / intervalTotalFrameCount;
|
|
if (lastConnectionStatusUpdate != CONN_STATUS_POOR &&
|
|
(frameLossPercent >= CONN_IMMEDIATE_POOR_LOSS_RATE ||
|
|
(frameLossPercent >= CONN_CONSECUTIVE_POOR_LOSS_RATE && lastIntervalLossPercentage >= CONN_CONSECUTIVE_POOR_LOSS_RATE))) {
|
|
// We require 2 consecutive intervals above CONN_CONSECUTIVE_POOR_LOSS_RATE or a single
|
|
// interval above CONN_IMMEDIATE_POOR_LOSS_RATE to notify of a poor connection.
|
|
ListenerCallbacks.connectionStatusUpdate(CONN_STATUS_POOR);
|
|
lastConnectionStatusUpdate = CONN_STATUS_POOR;
|
|
}
|
|
else if (frameLossPercent <= CONN_OKAY_LOSS_RATE && lastConnectionStatusUpdate != CONN_STATUS_OKAY) {
|
|
ListenerCallbacks.connectionStatusUpdate(CONN_STATUS_OKAY);
|
|
lastConnectionStatusUpdate = CONN_STATUS_OKAY;
|
|
}
|
|
|
|
lastIntervalLossPercentage = frameLossPercent;
|
|
}
|
|
|
|
// Reset interval
|
|
intervalStartTimeMs = now;
|
|
intervalGoodFrameCount = intervalTotalFrameCount = 0;
|
|
}
|
|
|
|
intervalTotalFrameCount += frameIndex - lastSeenFrame;
|
|
lastSeenFrame = frameIndex;
|
|
}
|
|
|
|
// When we lose packets, update our packet loss count
|
|
void connectionLostPackets(int lastReceivedPacket, int nextReceivedPacket) {
|
|
lossCountSinceLastReport += (nextReceivedPacket - lastReceivedPacket) - 1;
|
|
}
|
|
|
|
// Reads an NV control stream packet from the TCP connection
|
|
static PNVCTL_TCP_PACKET_HEADER readNvctlPacketTcp(void) {
|
|
NVCTL_TCP_PACKET_HEADER staticHeader;
|
|
PNVCTL_TCP_PACKET_HEADER fullPacket;
|
|
SOCK_RET err;
|
|
|
|
err = recv(ctlSock, (char*)&staticHeader, sizeof(staticHeader), 0);
|
|
if (err != sizeof(staticHeader)) {
|
|
return NULL;
|
|
}
|
|
|
|
staticHeader.type = LE16(staticHeader.type);
|
|
staticHeader.payloadLength = LE16(staticHeader.payloadLength);
|
|
|
|
fullPacket = (PNVCTL_TCP_PACKET_HEADER)malloc(staticHeader.payloadLength + sizeof(staticHeader));
|
|
if (fullPacket == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
memcpy(fullPacket, &staticHeader, sizeof(staticHeader));
|
|
if (staticHeader.payloadLength != 0) {
|
|
err = recv(ctlSock, (char*)(fullPacket + 1), staticHeader.payloadLength, 0);
|
|
if (err != staticHeader.payloadLength) {
|
|
free(fullPacket);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return fullPacket;
|
|
}
|
|
|
|
static bool encryptControlMessage(PNVCTL_ENCRYPTED_PACKET_HEADER encPacket, PNVCTL_ENET_PACKET_HEADER_V2 packet) {
|
|
unsigned char iv[16] = { 0 };
|
|
int encryptedSize = sizeof(*packet) + packet->payloadLength;
|
|
|
|
encPacket->encryptedHeaderType = LE16(encPacket->encryptedHeaderType);
|
|
encPacket->length = LE16(encPacket->length);
|
|
encPacket->seq = LE32(encPacket->seq);
|
|
|
|
packet->type = LE16(packet->type);
|
|
packet->payloadLength = LE16(packet->payloadLength);
|
|
|
|
// This is a truncating cast, but it's what Nvidia does, so we have to mimic it.
|
|
iv[0] = (unsigned char)encPacket->seq;
|
|
|
|
return PltEncryptMessage(encryptionCtx, ALGORITHM_AES_GCM, 0,
|
|
(unsigned char*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
|
|
iv, sizeof(iv),
|
|
(unsigned char*)(encPacket + 1), AES_GCM_TAG_LENGTH, // Write tag into the space after the encrypted header
|
|
(unsigned char*)packet, encryptedSize,
|
|
((unsigned char*)(encPacket + 1)) + AES_GCM_TAG_LENGTH, &encryptedSize); // Write ciphertext after the GCM tag
|
|
}
|
|
|
|
// Caller must free() *packet on success!!!
|
|
static bool decryptControlMessageToV1(PNVCTL_ENCRYPTED_PACKET_HEADER encPacket, int encPacketLength, PNVCTL_ENET_PACKET_HEADER_V1* packet, int* packetLength) {
|
|
unsigned char iv[16] = { 0 };
|
|
|
|
*packet = NULL;
|
|
|
|
// It must be an encrypted packet to begin with
|
|
LC_ASSERT(encPacket->encryptedHeaderType == 0x0001);
|
|
|
|
// Make sure the host isn't lying to us about the packet length
|
|
int expectedEncLength = encPacket->length + sizeof(encPacket->encryptedHeaderType) + sizeof(encPacket->length);
|
|
LC_ASSERT(encPacketLength == expectedEncLength);
|
|
if (encPacketLength < expectedEncLength) {
|
|
Limelog("Length exceeds packet boundary (needed %d, got %d)\n", expectedEncLength, encPacketLength);
|
|
return false;
|
|
}
|
|
|
|
// Check length first so we don't underflow
|
|
if (encPacket->length < sizeof(encPacket->seq) + AES_GCM_TAG_LENGTH + sizeof(NVCTL_ENET_PACKET_HEADER_V2)) {
|
|
Limelog("Received runt packet (%d). Unable to decrypt.\n", encPacket->length);
|
|
return false;
|
|
}
|
|
|
|
// This is a truncating cast, but it's what Nvidia does, so we have to mimic it.
|
|
iv[0] = (unsigned char)encPacket->seq;
|
|
|
|
int plaintextLength = encPacket->length - sizeof(encPacket->seq) - AES_GCM_TAG_LENGTH;
|
|
*packet = malloc(plaintextLength);
|
|
if (*packet == NULL) {
|
|
return false;
|
|
}
|
|
|
|
if (!PltDecryptMessage(decryptionCtx, ALGORITHM_AES_GCM, 0,
|
|
(unsigned char*)StreamConfig.remoteInputAesKey, sizeof(StreamConfig.remoteInputAesKey),
|
|
iv, sizeof(iv),
|
|
(unsigned char*)(encPacket + 1), AES_GCM_TAG_LENGTH, // The tag is located right after the header
|
|
((unsigned char*)(encPacket + 1)) + AES_GCM_TAG_LENGTH, plaintextLength, // The ciphertext is after the tag
|
|
(unsigned char*)*packet, &plaintextLength)) {
|
|
free(*packet);
|
|
return false;
|
|
}
|
|
|
|
// Now we do an in-place V2 to V1 header conversion, so our existing parsing code doesn't have to change.
|
|
// All we need to do is eliminate the new length field in V2 by shifting everything by 2 bytes.
|
|
memmove(((unsigned char*)*packet) + 2, ((unsigned char*)*packet) + 4, plaintextLength - 4);
|
|
*packetLength = plaintextLength - 2;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sendMessageEnet(short ptype, short paylen, const void* payload) {
|
|
ENetPacket* enetPacket;
|
|
int err;
|
|
|
|
LC_ASSERT(AppVersionQuad[0] >= 5);
|
|
|
|
if (encryptedControlStream) {
|
|
PNVCTL_ENCRYPTED_PACKET_HEADER encPacket;
|
|
PNVCTL_ENET_PACKET_HEADER_V2 packet;
|
|
char tempBuffer[256];
|
|
|
|
enetPacket = enet_packet_create(NULL,
|
|
sizeof(*encPacket) + AES_GCM_TAG_LENGTH + sizeof(*packet) + paylen,
|
|
ENET_PACKET_FLAG_RELIABLE);
|
|
if (enetPacket == NULL) {
|
|
return false;
|
|
}
|
|
|
|
// We (ab)use the enetMutex to protect currentEnetSequenceNumber and the cipherContext
|
|
// used inside encryptControlMessage().
|
|
PltLockMutex(&enetMutex);
|
|
|
|
encPacket = (PNVCTL_ENCRYPTED_PACKET_HEADER)enetPacket->data;
|
|
encPacket->encryptedHeaderType = 0x0001;
|
|
encPacket->length = sizeof(encPacket->seq) + AES_GCM_TAG_LENGTH + sizeof(*packet) + paylen;
|
|
encPacket->seq = currentEnetSequenceNumber++;
|
|
|
|
// Construct the plaintext data for encryption
|
|
LC_ASSERT(sizeof(*packet) + paylen < sizeof(tempBuffer));
|
|
packet = (PNVCTL_ENET_PACKET_HEADER_V2)tempBuffer;
|
|
packet->type = ptype;
|
|
packet->payloadLength = paylen;
|
|
memcpy(&packet[1], payload, paylen);
|
|
|
|
// Encrypt the data into the final packet (and byteswap for BE machines)
|
|
if (!encryptControlMessage(encPacket, packet)) {
|
|
Limelog("Failed to encrypt control stream message\n");
|
|
enet_packet_destroy(enetPacket);
|
|
PltUnlockMutex(&enetMutex);
|
|
return false;
|
|
}
|
|
|
|
PltUnlockMutex(&enetMutex);
|
|
}
|
|
else {
|
|
PNVCTL_ENET_PACKET_HEADER_V1 packet;
|
|
enetPacket = enet_packet_create(NULL, sizeof(*packet) + paylen, ENET_PACKET_FLAG_RELIABLE);
|
|
if (enetPacket == NULL) {
|
|
return false;
|
|
}
|
|
|
|
packet = (PNVCTL_ENET_PACKET_HEADER_V1)enetPacket->data;
|
|
packet->type = LE16(ptype);
|
|
memcpy(&packet[1], payload, paylen);
|
|
}
|
|
|
|
PltLockMutex(&enetMutex);
|
|
err = enet_peer_send(peer, 0, enetPacket);
|
|
PltUnlockMutex(&enetMutex);
|
|
if (err < 0) {
|
|
Limelog("Failed to send ENet control packet\n");
|
|
enet_packet_destroy(enetPacket);
|
|
return false;
|
|
}
|
|
|
|
PltLockMutex(&enetMutex);
|
|
enet_host_flush(client);
|
|
PltUnlockMutex(&enetMutex);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sendMessageTcp(short ptype, short paylen, const void* payload) {
|
|
PNVCTL_TCP_PACKET_HEADER packet;
|
|
SOCK_RET err;
|
|
|
|
LC_ASSERT(AppVersionQuad[0] < 5);
|
|
|
|
packet = malloc(sizeof(*packet) + paylen);
|
|
if (packet == NULL) {
|
|
return false;
|
|
}
|
|
|
|
packet->type = LE16(ptype);
|
|
packet->payloadLength = LE16(paylen);
|
|
memcpy(&packet[1], payload, paylen);
|
|
|
|
err = send(ctlSock, (char*) packet, sizeof(*packet) + paylen, 0);
|
|
free(packet);
|
|
|
|
if (err != (SOCK_RET)(sizeof(*packet) + paylen)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool sendMessageAndForget(short ptype, short paylen, const void* payload) {
|
|
bool ret;
|
|
|
|
// Unlike regular sockets, ENet sockets aren't safe to invoke from multiple
|
|
// threads at once. We have to synchronize them with a lock.
|
|
if (AppVersionQuad[0] >= 5) {
|
|
ret = sendMessageEnet(ptype, paylen, payload);
|
|
}
|
|
else {
|
|
ret = sendMessageTcp(ptype, paylen, payload);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static bool sendMessageAndDiscardReply(short ptype, short paylen, const void* payload) {
|
|
if (AppVersionQuad[0] >= 5) {
|
|
if (!sendMessageEnet(ptype, paylen, payload)) {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
PNVCTL_TCP_PACKET_HEADER reply;
|
|
|
|
if (!sendMessageTcp(ptype, paylen, payload)) {
|
|
return false;
|
|
}
|
|
|
|
// Discard the response
|
|
reply = readNvctlPacketTcp();
|
|
if (reply == NULL) {
|
|
return false;
|
|
}
|
|
|
|
free(reply);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// This intercept function drops disconnect events to allow us to process
|
|
// pending receives first. It works around what appears to be a bug in ENet
|
|
// where pending disconnects can cause loss of unprocessed received data.
|
|
static int ignoreDisconnectIntercept(ENetHost* host, ENetEvent* event) {
|
|
if (host->receivedDataLength == sizeof(ENetProtocolHeader) + sizeof(ENetProtocolDisconnect)) {
|
|
ENetProtocolHeader* protoHeader = (ENetProtocolHeader*)host->receivedData;
|
|
ENetProtocolDisconnect* disconnect = (ENetProtocolDisconnect*)(protoHeader + 1);
|
|
|
|
if ((disconnect->header.command & ENET_PROTOCOL_COMMAND_MASK) == ENET_PROTOCOL_COMMAND_DISCONNECT) {
|
|
Limelog("ENet disconnect event pending\n");
|
|
disconnectPending = true;
|
|
if (event) {
|
|
event->type = ENET_EVENT_TYPE_NONE;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void controlReceiveThreadFunc(void* context) {
|
|
int err;
|
|
|
|
// This is only used for ENet
|
|
if (AppVersionQuad[0] < 5) {
|
|
return;
|
|
}
|
|
|
|
while (!PltIsThreadInterrupted(&controlReceiveThread)) {
|
|
ENetEvent event;
|
|
|
|
// Poll for new packets and process retransmissions
|
|
PltLockMutex(&enetMutex);
|
|
err = serviceEnetHost(client, &event, 0);
|
|
PltUnlockMutex(&enetMutex);
|
|
if (err == 0) {
|
|
// Handle a pending disconnect after unsuccessfully polling
|
|
// for new events to handle.
|
|
if (disconnectPending) {
|
|
PltLockMutex(&enetMutex);
|
|
// Wait 100 ms for pending receives after a disconnect and
|
|
// 1 second for the pending disconnect to be processed after
|
|
// removing the intercept callback.
|
|
err = serviceEnetHost(client, &event, client->intercept ? 100 : 1000);
|
|
if (err == 0) {
|
|
if (client->intercept) {
|
|
// Now that no pending receive events remain, we can
|
|
// remove our intercept hook and allow the server's
|
|
// disconnect to be processed as expected. We will wait
|
|
// 1 second for this disconnect to be processed before
|
|
// we tear down the connection anyway.
|
|
client->intercept = NULL;
|
|
PltUnlockMutex(&enetMutex);
|
|
continue;
|
|
}
|
|
else {
|
|
// The 1 second timeout has expired with no disconnect event
|
|
// retransmission after the first notification. We can only
|
|
// assume the server died tragically, so go ahead and tear down.
|
|
PltUnlockMutex(&enetMutex);
|
|
Limelog("Disconnect event timeout expired\n");
|
|
ListenerCallbacks.connectionTerminated(-1);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
PltUnlockMutex(&enetMutex);
|
|
}
|
|
}
|
|
else {
|
|
// No events ready - sleep for a short time
|
|
//
|
|
// NOTE: This sleep *directly* impacts the lowest possible retransmission
|
|
// time for packets after a loss event. If we're busy sleeping here, we can't
|
|
// retransmit a dropped packet, so we keep the sleep time to a minimum.
|
|
PltSleepMsInterruptible(&controlReceiveThread, 10);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (err < 0) {
|
|
Limelog("Control stream connection failed: %d\n", err);
|
|
ListenerCallbacks.connectionTerminated(err);
|
|
return;
|
|
}
|
|
|
|
if (event.type == ENET_EVENT_TYPE_RECEIVE) {
|
|
PNVCTL_ENET_PACKET_HEADER_V1 ctlHdr;
|
|
int packetLength;
|
|
|
|
if (event.packet->dataLength < sizeof(*ctlHdr)) {
|
|
Limelog("Discarding runt control packet: %d < %d\n", event.packet->dataLength, (int)sizeof(*ctlHdr));
|
|
enet_packet_destroy(event.packet);
|
|
continue;
|
|
}
|
|
|
|
ctlHdr = (PNVCTL_ENET_PACKET_HEADER_V1)event.packet->data;
|
|
ctlHdr->type = LE16(ctlHdr->type);
|
|
|
|
if (encryptedControlStream) {
|
|
// V2 headers can be interpreted as V1 headers for the purpose of examining type,
|
|
// so this check is safe.
|
|
if (ctlHdr->type == 0x0001) {
|
|
PNVCTL_ENCRYPTED_PACKET_HEADER encHdr;
|
|
|
|
if (event.packet->dataLength < sizeof(NVCTL_ENCRYPTED_PACKET_HEADER)) {
|
|
Limelog("Discarding runt encrypted control packet: %d < %d\n", event.packet->dataLength, (int)sizeof(NVCTL_ENCRYPTED_PACKET_HEADER));
|
|
enet_packet_destroy(event.packet);
|
|
continue;
|
|
}
|
|
|
|
// encryptedHeaderType is already byteswapped by aliasing through ctlHdr above
|
|
encHdr = (PNVCTL_ENCRYPTED_PACKET_HEADER)event.packet->data;
|
|
encHdr->length = LE16(encHdr->length);
|
|
encHdr->seq = LE32(encHdr->seq);
|
|
|
|
ctlHdr = NULL;
|
|
packetLength = (int)event.packet->dataLength;
|
|
if (!decryptControlMessageToV1(encHdr, packetLength, &ctlHdr, &packetLength)) {
|
|
Limelog("Failed to decrypt control packet of size %d\n", event.packet->dataLength);
|
|
enet_packet_destroy(event.packet);
|
|
continue;
|
|
}
|
|
|
|
// We need to byteswap the unsealed header too
|
|
ctlHdr->type = LE16(ctlHdr->type);
|
|
}
|
|
else {
|
|
// What do we do here???
|
|
LC_ASSERT(false);
|
|
packetLength = (int)event.packet->dataLength;
|
|
}
|
|
}
|
|
else {
|
|
// Take ownership of the packet data directly for the non-encrypted case
|
|
packetLength = (int)event.packet->dataLength;
|
|
event.packet->data = NULL;
|
|
}
|
|
|
|
// We're done with the packet struct
|
|
enet_packet_destroy(event.packet);
|
|
|
|
// All below codepaths must free ctlHdr!!!
|
|
|
|
if (ctlHdr->type == packetTypes[IDX_RUMBLE_DATA]) {
|
|
BYTE_BUFFER bb;
|
|
|
|
BbInitializeWrappedBuffer(&bb, (char*)ctlHdr, sizeof(*ctlHdr), packetLength - sizeof(*ctlHdr), BYTE_ORDER_LITTLE);
|
|
BbAdvanceBuffer(&bb, 4);
|
|
|
|
uint16_t controllerNumber;
|
|
uint16_t lowFreqRumble;
|
|
uint16_t highFreqRumble;
|
|
|
|
BbGet16(&bb, &controllerNumber);
|
|
BbGet16(&bb, &lowFreqRumble);
|
|
BbGet16(&bb, &highFreqRumble);
|
|
|
|
ListenerCallbacks.rumble(controllerNumber, lowFreqRumble, highFreqRumble);
|
|
}
|
|
else if (ctlHdr->type == packetTypes[IDX_TERMINATION]) {
|
|
BYTE_BUFFER bb;
|
|
|
|
|
|
uint32_t terminationErrorCode;
|
|
|
|
if (packetLength >= 6) {
|
|
// This is the extended termination message which contains a full HRESULT
|
|
BbInitializeWrappedBuffer(&bb, (char*)ctlHdr, sizeof(*ctlHdr), packetLength - sizeof(*ctlHdr), BYTE_ORDER_BIG);
|
|
BbGet32(&bb, &terminationErrorCode);
|
|
|
|
Limelog("Server notified termination reason: 0x%08x\n", terminationErrorCode);
|
|
|
|
// NVST_DISCONN_SERVER_TERMINATED_CLOSED is the expected graceful termination error
|
|
if (terminationErrorCode == 0x80030023) {
|
|
if (lastSeenFrame != 0) {
|
|
// Pass error code 0 to notify the client that this was not an error
|
|
terminationErrorCode = ML_ERROR_GRACEFUL_TERMINATION;
|
|
}
|
|
else {
|
|
// We never saw a frame, so this is probably an error that caused
|
|
// NvStreamer to terminate prior to sending any frames.
|
|
terminationErrorCode = ML_ERROR_UNEXPECTED_EARLY_TERMINATION;
|
|
}
|
|
}
|
|
// NVST_DISCONN_SERVER_VFP_PROTECTED_CONTENT means it failed due to protected content on screen
|
|
else if (terminationErrorCode == 0x800e9302) {
|
|
terminationErrorCode = ML_ERROR_PROTECTED_CONTENT;
|
|
}
|
|
}
|
|
else {
|
|
uint16_t terminationReason;
|
|
|
|
// This is the short termination message
|
|
BbInitializeWrappedBuffer(&bb, (char*)ctlHdr, sizeof(*ctlHdr), packetLength - sizeof(*ctlHdr), BYTE_ORDER_LITTLE);
|
|
BbGet16(&bb, &terminationReason);
|
|
|
|
Limelog("Server notified termination reason: 0x%04x\n", terminationReason);
|
|
|
|
// SERVER_TERMINATED_INTENDED
|
|
if (terminationReason == 0x0100) {
|
|
if (lastSeenFrame != 0) {
|
|
// Pass error code 0 to notify the client that this was not an error
|
|
terminationErrorCode = ML_ERROR_GRACEFUL_TERMINATION;
|
|
}
|
|
else {
|
|
// We never saw a frame, so this is probably an error that caused
|
|
// NvStreamer to terminate prior to sending any frames.
|
|
terminationErrorCode = ML_ERROR_UNEXPECTED_EARLY_TERMINATION;
|
|
}
|
|
}
|
|
else {
|
|
// Otherwise pass the reason unmodified
|
|
terminationErrorCode = terminationReason;
|
|
}
|
|
}
|
|
|
|
// We used to wait for a ENET_EVENT_TYPE_DISCONNECT event, but since
|
|
// GFE 3.20.3.63 we don't get one for 10 seconds after we first get
|
|
// this termination message. The termination message should be reliable
|
|
// enough to end the stream now, rather than waiting for an explicit
|
|
// disconnect.
|
|
ListenerCallbacks.connectionTerminated((int)terminationErrorCode);
|
|
free(ctlHdr);
|
|
return;
|
|
}
|
|
|
|
free(ctlHdr);
|
|
}
|
|
else if (event.type == ENET_EVENT_TYPE_DISCONNECT) {
|
|
Limelog("Control stream received unexpected disconnect event\n");
|
|
ListenerCallbacks.connectionTerminated(-1);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void lossStatsThreadFunc(void* context) {
|
|
BYTE_BUFFER byteBuffer;
|
|
|
|
if (usePeriodicPing) {
|
|
char periodicPingPayload[8];
|
|
|
|
BbInitializeWrappedBuffer(&byteBuffer, periodicPingPayload, 0, sizeof(periodicPingPayload), BYTE_ORDER_LITTLE);
|
|
BbPut16(&byteBuffer, 4); // Length of payload
|
|
BbPut32(&byteBuffer, 0); // Timestamp?
|
|
|
|
while (!PltIsThreadInterrupted(&lossStatsThread)) {
|
|
// Send the message (and don't expect a response)
|
|
if (!sendMessageAndForget(0x0200, sizeof(periodicPingPayload), periodicPingPayload)) {
|
|
Limelog("Loss Stats: Transaction failed: %d\n", (int)LastSocketError());
|
|
ListenerCallbacks.connectionTerminated(LastSocketFail());
|
|
return;
|
|
}
|
|
|
|
// Wait a bit
|
|
PltSleepMsInterruptible(&lossStatsThread, PERIODIC_PING_INTERVAL_MS);
|
|
}
|
|
}
|
|
else {
|
|
char* lossStatsPayload;
|
|
|
|
lossStatsPayload = malloc(payloadLengths[IDX_LOSS_STATS]);
|
|
if (lossStatsPayload == NULL) {
|
|
Limelog("Loss Stats: malloc() failed\n");
|
|
ListenerCallbacks.connectionTerminated(-1);
|
|
return;
|
|
}
|
|
|
|
while (!PltIsThreadInterrupted(&lossStatsThread)) {
|
|
// Construct the payload
|
|
BbInitializeWrappedBuffer(&byteBuffer, lossStatsPayload, 0, payloadLengths[IDX_LOSS_STATS], BYTE_ORDER_LITTLE);
|
|
BbPut32(&byteBuffer, lossCountSinceLastReport);
|
|
BbPut32(&byteBuffer, LOSS_REPORT_INTERVAL_MS);
|
|
BbPut32(&byteBuffer, 1000);
|
|
BbPut64(&byteBuffer, lastGoodFrame);
|
|
BbPut32(&byteBuffer, 0);
|
|
BbPut32(&byteBuffer, 0);
|
|
BbPut32(&byteBuffer, 0x14);
|
|
|
|
// Send the message (and don't expect a response)
|
|
if (!sendMessageAndForget(packetTypes[IDX_LOSS_STATS],
|
|
payloadLengths[IDX_LOSS_STATS], lossStatsPayload)) {
|
|
free(lossStatsPayload);
|
|
Limelog("Loss Stats: Transaction failed: %d\n", (int)LastSocketError());
|
|
ListenerCallbacks.connectionTerminated(LastSocketFail());
|
|
return;
|
|
}
|
|
|
|
// Clear the transient state
|
|
lossCountSinceLastReport = 0;
|
|
|
|
// Wait a bit
|
|
PltSleepMsInterruptible(&lossStatsThread, LOSS_REPORT_INTERVAL_MS);
|
|
}
|
|
|
|
free(lossStatsPayload);
|
|
}
|
|
}
|
|
|
|
static void requestIdrFrame(void) {
|
|
// If this server does not have a known IDR frame request
|
|
// message, we'll accomplish the same thing by creating a
|
|
// reference frame invalidation request.
|
|
if (!supportsIdrFrameRequest) {
|
|
int64_t payload[3];
|
|
|
|
// Form the payload
|
|
if (lastSeenFrame < 0x20) {
|
|
payload[0] = 0;
|
|
payload[1] = LE64(lastSeenFrame);
|
|
}
|
|
else {
|
|
payload[0] = LE64(lastSeenFrame - 0x20);
|
|
payload[1] = LE64(lastSeenFrame);
|
|
}
|
|
|
|
payload[2] = 0;
|
|
|
|
// Send the reference frame invalidation request and read the response
|
|
if (!sendMessageAndDiscardReply(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
|
payloadLengths[IDX_INVALIDATE_REF_FRAMES], payload)) {
|
|
Limelog("Request IDR Frame: Transaction failed: %d\n", (int)LastSocketError());
|
|
ListenerCallbacks.connectionTerminated(LastSocketFail());
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
// Send IDR frame request and read the response
|
|
if (!sendMessageAndDiscardReply(packetTypes[IDX_REQUEST_IDR_FRAME],
|
|
payloadLengths[IDX_REQUEST_IDR_FRAME], preconstructedPayloads[IDX_REQUEST_IDR_FRAME])) {
|
|
Limelog("Request IDR Frame: Transaction failed: %d\n", (int)LastSocketError());
|
|
ListenerCallbacks.connectionTerminated(LastSocketFail());
|
|
return;
|
|
}
|
|
}
|
|
|
|
Limelog("IDR frame request sent\n");
|
|
}
|
|
|
|
static void requestInvalidateReferenceFrames(int startFrame, int endFrame) {
|
|
int64_t payload[3];
|
|
|
|
LC_ASSERT(startFrame <= endFrame);
|
|
LC_ASSERT(isReferenceFrameInvalidationEnabled());
|
|
|
|
payload[0] = LE64(startFrame);
|
|
payload[1] = LE64(endFrame);
|
|
payload[2] = 0;
|
|
|
|
// Send the reference frame invalidation request and read the response
|
|
if (!sendMessageAndDiscardReply(packetTypes[IDX_INVALIDATE_REF_FRAMES],
|
|
payloadLengths[IDX_INVALIDATE_REF_FRAMES], payload)) {
|
|
Limelog("Request Invaldiate Reference Frames: Transaction failed: %d\n", (int)LastSocketError());
|
|
ListenerCallbacks.connectionTerminated(LastSocketFail());
|
|
return;
|
|
}
|
|
|
|
Limelog("Invalidate reference frame request sent (%d to %d)\n", startFrame, endFrame);
|
|
}
|
|
|
|
static void invalidateRefFramesFunc(void* context) {
|
|
LC_ASSERT(isReferenceFrameInvalidationEnabled());
|
|
|
|
while (!PltIsThreadInterrupted(&invalidateRefFramesThread)) {
|
|
PQUEUED_FRAME_INVALIDATION_TUPLE qfit;
|
|
int startFrame;
|
|
int endFrame;
|
|
|
|
// Wait for a reference frame invalidation request or a request to shutdown
|
|
if (LbqWaitForQueueElement(&invalidReferenceFrameTuples, (void**)&qfit) != LBQ_SUCCESS) {
|
|
// Bail if we're stopping
|
|
return;
|
|
}
|
|
|
|
startFrame = qfit->startFrame;
|
|
endFrame = qfit->endFrame;
|
|
|
|
// Aggregate all lost frames into one range
|
|
do {
|
|
LC_ASSERT(qfit->endFrame >= endFrame);
|
|
endFrame = qfit->endFrame;
|
|
free(qfit);
|
|
} while (LbqPollQueueElement(&invalidReferenceFrameTuples, (void**)&qfit) == LBQ_SUCCESS);
|
|
|
|
// Send the reference frame invalidation request
|
|
requestInvalidateReferenceFrames(startFrame, endFrame);
|
|
}
|
|
}
|
|
|
|
static void requestIdrFrameFunc(void* context) {
|
|
while (!PltIsThreadInterrupted(&requestIdrFrameThread)) {
|
|
PltWaitForEvent(&idrFrameRequiredEvent);
|
|
PltClearEvent(&idrFrameRequiredEvent);
|
|
|
|
if (stopping) {
|
|
// Bail if we're stopping
|
|
return;
|
|
}
|
|
|
|
// Any pending reference frame invalidation requests are now redundant
|
|
freeFrameInvalidationList(LbqFlushQueueItems(&invalidReferenceFrameTuples));
|
|
|
|
// Request the IDR frame
|
|
requestIdrFrame();
|
|
}
|
|
}
|
|
|
|
// Stops the control stream
|
|
int stopControlStream(void) {
|
|
stopping = true;
|
|
LbqSignalQueueShutdown(&invalidReferenceFrameTuples);
|
|
PltSetEvent(&idrFrameRequiredEvent);
|
|
|
|
// This must be set to stop in a timely manner
|
|
LC_ASSERT(ConnectionInterrupted);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
shutdownTcpSocket(ctlSock);
|
|
}
|
|
|
|
PltInterruptThread(&lossStatsThread);
|
|
PltInterruptThread(&requestIdrFrameThread);
|
|
PltInterruptThread(&controlReceiveThread);
|
|
|
|
PltJoinThread(&lossStatsThread);
|
|
PltJoinThread(&requestIdrFrameThread);
|
|
PltJoinThread(&controlReceiveThread);
|
|
|
|
PltCloseThread(&lossStatsThread);
|
|
PltCloseThread(&requestIdrFrameThread);
|
|
PltCloseThread(&controlReceiveThread);
|
|
|
|
// We will only have an RFI thread if RFI is enabled
|
|
if (isReferenceFrameInvalidationEnabled()) {
|
|
PltInterruptThread(&invalidateRefFramesThread);
|
|
PltJoinThread(&invalidateRefFramesThread);
|
|
PltCloseThread(&invalidateRefFramesThread);
|
|
}
|
|
|
|
if (peer != NULL) {
|
|
if (UserRequestedTermination) {
|
|
// Gracefully disconnect to ensure the remote host receives all of our final
|
|
// outbound traffic, including any key up events that might be sent.
|
|
gracefullyDisconnectEnetPeer(client, peer, CONTROL_STREAM_LINGER_TIMEOUT_SEC * 1000);
|
|
enet_peer_reset(peer);
|
|
}
|
|
else {
|
|
// This termination was requested by the remote host or caused due to a
|
|
// connection problem, so just do a quick abortive disconnect. The peer
|
|
// may be gone by this point.
|
|
enet_peer_disconnect_now(peer, 0);
|
|
}
|
|
|
|
peer = NULL;
|
|
}
|
|
if (client != NULL) {
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Called by the input stream to send a packet for Gen 5+ servers
|
|
int sendInputPacketOnControlStream(unsigned char* data, int length) {
|
|
LC_ASSERT(AppVersionQuad[0] >= 5);
|
|
|
|
// Send the input data (no reply expected)
|
|
if (sendMessageAndForget(packetTypes[IDX_INPUT_DATA], length, data) == 0) {
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// Starts the control stream
|
|
int startControlStream(void) {
|
|
int err;
|
|
|
|
if (AppVersionQuad[0] >= 5) {
|
|
ENetAddress address;
|
|
ENetEvent event;
|
|
|
|
enet_address_set_address(&address, (struct sockaddr *)&RemoteAddr, RemoteAddrLen);
|
|
enet_address_set_port(&address, 47999);
|
|
|
|
// Create a client that can use 1 outgoing connection and 1 channel
|
|
client = enet_host_create(address.address.ss_family, NULL, 1, 1, 0, 0);
|
|
if (client == NULL) {
|
|
stopping = true;
|
|
return -1;
|
|
}
|
|
|
|
client->intercept = ignoreDisconnectIntercept;
|
|
|
|
// Connect to the host
|
|
peer = enet_host_connect(client, &address, 1, 0);
|
|
if (peer == NULL) {
|
|
stopping = true;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
return -1;
|
|
}
|
|
|
|
// Wait for the connect to complete
|
|
if (serviceEnetHost(client, &event, CONTROL_STREAM_TIMEOUT_SEC * 1000) <= 0 ||
|
|
event.type != ENET_EVENT_TYPE_CONNECT) {
|
|
Limelog("Failed to connect to UDP port 47999\n");
|
|
stopping = true;
|
|
enet_peer_reset(peer);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
return ETIMEDOUT;
|
|
}
|
|
|
|
// Ensure the connect verify ACK is sent immediately
|
|
enet_host_flush(client);
|
|
|
|
// Set the peer timeout to 10 seconds and limit backoff to 2x RTT
|
|
enet_peer_timeout(peer, 2, 10000, 10000);
|
|
}
|
|
else {
|
|
ctlSock = connectTcpSocket(&RemoteAddr, RemoteAddrLen,
|
|
47995, CONTROL_STREAM_TIMEOUT_SEC);
|
|
if (ctlSock == INVALID_SOCKET) {
|
|
stopping = true;
|
|
return LastSocketFail();
|
|
}
|
|
|
|
enableNoDelay(ctlSock);
|
|
}
|
|
|
|
err = PltCreateThread("ControlRecv", controlReceiveThreadFunc, NULL, &controlReceiveThread);
|
|
if (err != 0) {
|
|
stopping = true;
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
else {
|
|
enet_peer_disconnect_now(peer, 0);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
// Send START A
|
|
if (!sendMessageAndDiscardReply(packetTypes[IDX_START_A],
|
|
payloadLengths[IDX_START_A],
|
|
preconstructedPayloads[IDX_START_A])) {
|
|
Limelog("Start A failed: %d\n", (int)LastSocketError());
|
|
err = LastSocketFail();
|
|
stopping = true;
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
shutdownTcpSocket(ctlSock);
|
|
}
|
|
else {
|
|
ConnectionInterrupted = true;
|
|
}
|
|
|
|
PltInterruptThread(&controlReceiveThread);
|
|
PltJoinThread(&controlReceiveThread);
|
|
PltCloseThread(&controlReceiveThread);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
else {
|
|
enet_peer_disconnect_now(peer, 0);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
// Send START B
|
|
if (!sendMessageAndDiscardReply(packetTypes[IDX_START_B],
|
|
payloadLengths[IDX_START_B],
|
|
preconstructedPayloads[IDX_START_B])) {
|
|
Limelog("Start B failed: %d\n", (int)LastSocketError());
|
|
err = LastSocketFail();
|
|
stopping = true;
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
shutdownTcpSocket(ctlSock);
|
|
}
|
|
else {
|
|
ConnectionInterrupted = true;
|
|
}
|
|
|
|
PltInterruptThread(&controlReceiveThread);
|
|
PltJoinThread(&controlReceiveThread);
|
|
PltCloseThread(&controlReceiveThread);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
else {
|
|
enet_peer_disconnect_now(peer, 0);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
err = PltCreateThread("LossStats", lossStatsThreadFunc, NULL, &lossStatsThread);
|
|
if (err != 0) {
|
|
stopping = true;
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
shutdownTcpSocket(ctlSock);
|
|
}
|
|
else {
|
|
ConnectionInterrupted = true;
|
|
}
|
|
|
|
PltInterruptThread(&controlReceiveThread);
|
|
PltJoinThread(&controlReceiveThread);
|
|
PltCloseThread(&controlReceiveThread);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
else {
|
|
enet_peer_disconnect_now(peer, 0);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
err = PltCreateThread("ReqIdrFrame", requestIdrFrameFunc, NULL, &requestIdrFrameThread);
|
|
if (err != 0) {
|
|
stopping = true;
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
shutdownTcpSocket(ctlSock);
|
|
}
|
|
else {
|
|
ConnectionInterrupted = true;
|
|
}
|
|
|
|
PltInterruptThread(&lossStatsThread);
|
|
PltJoinThread(&lossStatsThread);
|
|
PltCloseThread(&lossStatsThread);
|
|
|
|
PltInterruptThread(&controlReceiveThread);
|
|
PltJoinThread(&controlReceiveThread);
|
|
PltCloseThread(&controlReceiveThread);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
else {
|
|
enet_peer_disconnect_now(peer, 0);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
// Only create the reference frame invalidation thread if RFI is enabled
|
|
if (isReferenceFrameInvalidationEnabled()) {
|
|
err = PltCreateThread("InvRefFrames", invalidateRefFramesFunc, NULL, &invalidateRefFramesThread);
|
|
if (err != 0) {
|
|
stopping = true;
|
|
PltSetEvent(&idrFrameRequiredEvent);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
shutdownTcpSocket(ctlSock);
|
|
}
|
|
else {
|
|
ConnectionInterrupted = true;
|
|
}
|
|
|
|
PltInterruptThread(&lossStatsThread);
|
|
PltJoinThread(&lossStatsThread);
|
|
PltCloseThread(&lossStatsThread);
|
|
|
|
PltInterruptThread(&controlReceiveThread);
|
|
PltJoinThread(&controlReceiveThread);
|
|
PltCloseThread(&controlReceiveThread);
|
|
|
|
PltInterruptThread(&requestIdrFrameThread);
|
|
PltJoinThread(&requestIdrFrameThread);
|
|
PltCloseThread(&requestIdrFrameThread);
|
|
|
|
if (ctlSock != INVALID_SOCKET) {
|
|
closeSocket(ctlSock);
|
|
ctlSock = INVALID_SOCKET;
|
|
}
|
|
else {
|
|
enet_peer_disconnect_now(peer, 0);
|
|
peer = NULL;
|
|
enet_host_destroy(client);
|
|
client = NULL;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|