diff --git a/limelight-common/Limelight.h b/limelight-common/Limelight.h index 60b4e3c..d7058c1 100644 --- a/limelight-common/Limelight.h +++ b/limelight-common/Limelight.h @@ -10,6 +10,8 @@ typedef struct _STREAM_CONFIGURATION { int width; int height; int fps; + int bitrate; + int packetSize; } STREAM_CONFIGURATION, *PSTREAM_CONFIGURATION; typedef struct _LENTRY { diff --git a/limelight-common/SdpGenerator.c b/limelight-common/SdpGenerator.c new file mode 100644 index 0000000..d087ded --- /dev/null +++ b/limelight-common/SdpGenerator.c @@ -0,0 +1,369 @@ +#include "Limelight-internal.h" + +#define MAX_OPTION_NAME_LEN 128 + +#define MAX_SDP_HEADER_LEN 128 +#define MAX_SDP_TAIL_LEN 128 + +static const char ATTRIB_X_NV_CALLBACKS [] = { + 0x50, 0x51, 0x49, 0x4a, 0x0d, + 0xad, 0x30, 0x4a, 0xf1, 0xbd, 0x30, 0x4a, 0xd5, + 0xac, 0x30, 0x4a, 0x21, 0xbc, 0x30, 0x4a, 0xc1, + 0xbb, 0x30, 0x4a, 0x7d, 0xbb, 0x30, 0x4a, 0x19, + 0xbb, 0x30, 0x4a, 0x00, 0x00, 0x00, 0x00 +}; + +static const char ATTRIB_X_NV_VIDEO_DECODER [] = { + 0x50, 0x51, 0x49, 0x4a, 0x65, 0xad, 0x30, 0x4a, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd1, 0xac, 0x30, + 0x4a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x4d, 0xad, 0x30, 0x4a +}; + +static const char ATTRIB_X_NV_AUDIO_RENDERER [] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +static const int ATTRIB_X_NV_GENERAL_SERVERPORTS [] = { + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff, + + 0x00000000, 0xffffffff, 0xffffffff, 0x00000000, + 0xffffffff, 0xffffffff, 0x00000000, 0xffffffff, + 0xffffffff, 0x00000000, 0xffffffff, 0xffffffff +}; + +typedef struct _SDP_OPTION { + char name[MAX_OPTION_NAME_LEN+1]; + void* payload; + int payloadLen; + struct _SDP_OPTION *next; +} SDP_OPTION, *PSDP_OPTION; + +static void freeAttributeList(PSDP_OPTION head) { + PSDP_OPTION next; + while (head != NULL) { + next = head->next; + free(head); + head = next; + } +} + +static int getSerializedAttributeListSize(PSDP_OPTION head) { + PSDP_OPTION currentEntry = head; + int size = 0; + while (currentEntry != NULL) { + size += strlen("a="); + size += strlen(currentEntry->name); + size += strlen(":"); + size += currentEntry->payloadLen; + size += strlen(" \r\n"); + + currentEntry = currentEntry->next; + } + return size; +} + +static int fillSerializedAttributeList(char* buffer, PSDP_OPTION head) { + PSDP_OPTION currentEntry = head; + int offset = 0; + while (currentEntry != NULL) { + offset += sprintf(&buffer[offset], "a=%s:", currentEntry->name); + memcpy(&buffer[offset], currentEntry->payload, currentEntry->payloadLen); + offset += currentEntry->payloadLen; + offset += sprintf(&buffer[offset], " \r\n"); + + currentEntry = currentEntry->next; + } + return offset; +} + +static int addAttributeBinary(PSDP_OPTION *head, char* name, void* payload, int payloadLen) { + PSDP_OPTION option, currentOption; + + option = malloc(sizeof(*option) + payloadLen); + if (option == NULL) { + return -1; + } + + option->payloadLen = payloadLen; + strcpy(option->name, name); + option->payload = (void*)(option + 1); + memcpy(option->payload, payload, payloadLen); + + if (*head == NULL) { + *head = option; + } + else { + currentOption = *head; + while (currentOption->next != NULL) { + currentOption = currentOption->next; + } + currentOption->next = option; + } + + return 0; +} + +static int addAttributeString(PSDP_OPTION *head, char* name, char* payload) { + // We purposefully omit the null terminating character + return addAttributeBinary(head, name, payload, strlen(payload)); +} + +static PSDP_OPTION *getAttributesList(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress) { + PSDP_OPTION optionHead; + int payloadInt; + char payloadStr[64]; + void* payloadBinary; + int err; + + optionHead = NULL; + + err |= addAttributeBinary(&optionHead, "x-nv-callbacks", + ATTRIB_X_NV_CALLBACKS, sizeof(ATTRIB_X_NV_CALLBACKS)); + err |= addAttributeBinary(&optionHead, "x-nv-videoDecoder", + ATTRIB_X_NV_VIDEO_DECODER, sizeof(ATTRIB_X_NV_VIDEO_DECODER)); + err |= addAttributeBinary(&optionHead, "x-nv-audioRenderer", + ATTRIB_X_NV_AUDIO_RENDERER, sizeof(ATTRIB_X_NV_AUDIO_RENDERER)); + err |= addAttributeString(&optionHead, "x-nv-general.serverAddress", + inet_ntoa(targetAddress)); + err |= addAttributeBinary(&optionHead, "x-nv-general.serverPorts", + ATTRIB_X_NV_GENERAL_SERVERPORTS, sizeof(ATTRIB_X_NV_GENERAL_SERVERPORTS)); + + err |= addAttributeString(&optionHead, "x-nv-general.videoSyncAudioDelayAdjust", "10000"); + err |= addAttributeString(&optionHead, "x-nv-general.startTime", "0"); + payloadInt = 0xFFFFFFFF; + err |= addAttributeBinary(&optionHead, + "x-nv-general.featureFlags", &payloadInt, sizeof(payloadInt)); + err |= addAttributeString(&optionHead, "x-nv-general.userIdleWarningTimeout", "0"); + err |= addAttributeString(&optionHead, "x-nv-general.userIdleSessionTimeout", "0"); + err |= addAttributeString(&optionHead, "x-nv-general.serverCapture", "0"); + err |= addAttributeString(&optionHead, "x-nv-general.clientCapture", "0"); + err |= addAttributeString(&optionHead, "x-nv-general.rtpQueueMaxPackets", "16"); + err |= addAttributeString(&optionHead, "x-nv-general.rtpQueueMaxDurationMs", "40"); + err |= addAttributeString(&optionHead, "x-nv-general.useRtspClient", "257"); + + sprintf(payloadStr, "%d", streamConfig->width); + err |= addAttributeString(&optionHead, "x-nv-video[0].clientViewportWd", payloadStr); + sprintf(payloadStr, "%d", streamConfig->height); + err |= addAttributeString(&optionHead, "x-nv-video[0].clientViewportHt", payloadStr); + + err |= addAttributeString(&optionHead, "x-nv-video[0].adapterNumber", "0"); + + sprintf(payloadStr, "%d", streamConfig->fps); + err |= addAttributeString(&optionHead, "x-nv-video[0].maxFPS", payloadStr); + + err |= addAttributeString(&optionHead, "x-nv-video[0].iFrameOnDemand", "1"); + + payloadInt = 1; + err |= addAttributeBinary(&optionHead, + "x-nv-video[0].transferProtocol", &payloadInt, sizeof(payloadInt)); + if (streamConfig->height >= 1080 && streamConfig->fps >= 60) { + payloadInt = 4; + err |= addAttributeBinary(&optionHead, + "x-nv-video[0].rateControlMode", &payloadInt, sizeof(payloadInt)); + err |= addAttributeString(&optionHead, "x-nv-video[0].averageBitrate", "30"); + err |= addAttributeString(&optionHead, "x-nv-video[0].peakBitrate", "30"); + } + else { + payloadInt = 5; + err |= addAttributeBinary(&optionHead, + "x-nv-video[0].rateControlMode", &payloadInt, sizeof(payloadInt)); + err |= addAttributeString(&optionHead, "x-nv-video[0].averageBitrate", "7"); + err |= addAttributeString(&optionHead, "x-nv-video[0].peakBitrate", "7"); + } + + err |= addAttributeString(&optionHead, "x-nv-video[0].gopLength", "60"); + err |= addAttributeString(&optionHead, "x-nv-video[0].vbvMultiplier", "100"); + err |= addAttributeString(&optionHead, "x-nv-video[0].slicesPerFrame", "4"); + err |= addAttributeString(&optionHead, "x-nv-video[0].numTemporalLayers", "0"); + sprintf(payloadStr, "%d", streamConfig->packetSize); + err |= addAttributeString(&optionHead, "x-nv-video[0].packetSize", payloadStr); + err |= addAttributeString(&optionHead, "x-nv-video[0].enableSubframeEncoding", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].refPicInvalidation", "1"); + err |= addAttributeString(&optionHead, "x-nv-video[0].pingBackIntervalMs", "3000"); + err |= addAttributeString(&optionHead, "x-nv-video[0].pingBackTimeoutMs", "10000"); + err |= addAttributeString(&optionHead, "x-nv-video[0].timeoutLengthMs", "7000"); + err |= addAttributeString(&optionHead, "x-nv-video[0].fullFrameAssembly", "1"); + err |= addAttributeString(&optionHead, "x-nv-video[0].decodeIncompleteFrames", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].enableIntraRefresh", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].enableLongTermReferences", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].enableFrameRateCtrl", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].rtpDynamicPort", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].framesWithInvalidRefThreshold", "0"); + err |= addAttributeString(&optionHead, "x-nv-video[0].consecutiveFrameLostThreshold", "0"); + + err |= addAttributeString(&optionHead, "x-nv-vqos[0].ts.enable", "0"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].ts.averageBitrate", "8"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].ts.maximumBitrate", "10"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.flags", "819"); // Bit 2 being set causes picture problems (should be 823) + + // We clamp to min = max so manual bitrate settings take effect without time to scale up + sprintf(payloadStr, "%d", streamConfig->bitrate); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.maximumBitrate", payloadStr); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.minimumBitrate", payloadStr); + + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.statsTime", "50"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.zeroLossCount", "3000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.lossThreshold", "2"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.owdThreshold", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.owdReference", "500"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.lossWaitTime", "75"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.rateDropMultiplier", "25"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.rateGainMultiplier", "10"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.maxFps", "60"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.minFps", "30"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.fpsThreshold", "3"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.jitterThreshold", "1000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.jitterWaitTime", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.noJitterWaitTime", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionEnableBitRatePercentThreshold", "110"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionEnableL1Threshold", "10"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionEnableL0Threshold", "6"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionDisableThreshold", "4"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionDisableWaitTime", "20000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionDisableWaitPercent", "100"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionLowerBoundRate", "1000"); + + if (streamConfig->height >= 1080) { + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionLowerBoundWidth", "1280"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionLowerBoundHeight", "720"); + } + else { + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionLowerBoundWidth", "720"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.earlyDetectionLowerBoundHeight", "480"); + } + + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.enableFlags", "3"); + + if (streamConfig->height >= 1080 && streamConfig->fps >= 60) { + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.lowBitrate30FpsThreshold", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.lowBitrate60FpsThreshold", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.highBitrateThreshold", "7000"); + } + else { + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.lowBitrate30FpsThreshold", "4000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.lowBitrate60FpsThreshold", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.highBitrateThreshold", "6000"); + } + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.pf.bitrateStepSize", "1000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bn.notifyUpBoundThreshold", "40"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bn.notifyLowBoundThreshold", "25"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].bn.notifyWaitTime", "3000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].fec.enable", "1"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].fec.numSrcPackets", "50"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].fec.numOutPackets", "60"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].fec.repairPercent", "20"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].pictureRefreshIntervalMs", "0"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].videoQualityScoreUpdateTime", "5000"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].invalidateThreshold", "3"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].invalidateSkipPercentage", "10"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].qosTrafficType", "7"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].videoQoSMaxRoundTripLatencyFrames", "12"); + err |= addAttributeString(&optionHead, "x-nv-vqos[0].videoQoSMaxConsecutiveDrops", "3"); + payloadInt = 1; + err |= addAttributeBinary(&optionHead, "x-nv-vqos[0].profile", &payloadInt, sizeof(payloadInt)); + + payloadInt = 1; + err |= addAttributeBinary(&optionHead, "x-nv-aqos.mode", &payloadInt, sizeof(payloadInt)); + err |= addAttributeString(&optionHead, "x-nv-aqos.enableAudioStats", "1"); + err |= addAttributeString(&optionHead, "x-nv-aqos.audioStatsUpdateIntervalMs", "70"); + err |= addAttributeString(&optionHead, "x-nv-aqos.enablePacketLossPercentage", "1"); + err |= addAttributeString(&optionHead, "x-nv-aqos.bitRate", "96000"); + err |= addAttributeString(&optionHead, "x-nv-aqos.packetDuration", "5"); + err |= addAttributeString(&optionHead, "x-nv-aqos.packetLossPercentageUpdateIntervalMs", "100"); + err |= addAttributeString(&optionHead, "x-nv-aqos.qosTrafficType", "4"); + + err |= addAttributeString(&optionHead, "x-nv-runtime.recordClientStats", "8"); + err |= addAttributeString(&optionHead, "x-nv-runtime.recordServerStats", "0"); + err |= addAttributeString(&optionHead, "x-nv-runtime.clientNetworkCapture", "0"); + err |= addAttributeString(&optionHead, "x-nv-runtime.clientTraceCapture", "0"); + err |= addAttributeString(&optionHead, "x-nv-runtime.serverNetworkCapture", "0"); + err |= addAttributeString(&optionHead, "x-nv-runtime.serverTraceCapture", "0"); + + payloadInt = 0; + err |= addAttributeBinary(&optionHead, "x-nv-ri.protocol", &payloadInt, sizeof(payloadInt)); + err |= addAttributeString(&optionHead, "x-nv-ri.sendStatus", "0"); + payloadInt = 0; + err |= addAttributeBinary(&optionHead, "x-nv-ri.securityProtocol", &payloadInt, sizeof(payloadInt)); + payloadBinary = calloc(1, 0x20a); + if (payloadBinary == NULL) { + goto ExitFailure; + } + err |= addAttributeBinary(&optionHead, "x-nv-ri.secInfo", payloadBinary, 0x20a); + free(payloadBinary); + err |= addAttributeString(&optionHead, "x-nv-videoFrameDropIntervalNumber", "0"); + err |= addAttributeString(&optionHead, "x-nv-videoFrameDropContinualNumber", "0"); + + if (err == 0) { + return optionHead; + } + +ExitFailure: + freeAttributeList(optionHead); + return NULL; +} + +int fillSdpHeader(char* buffer, struct in_addr targetAddress) { + return sprintf(buffer, + "v=0\r\n" + "o=android 0 9 IN IPv4 %s\r\n" + "s=NVIDIA Streaming Client\r\n", inet_ntoa(targetAddress)); +} + +int fillSdpTail(char* buffer) { + return sprintf(buffer, + "t=0 0\r\n" + "m=video 47996 \r\n"); +} + +char* getSdpPayloadForStreamConfig(PSTREAM_CONFIGURATION streamConfig, struct in_addr targetAddress, int *length) { + PSDP_OPTION attributeList; + int offset; + char* payload; + + attributeList = getAttributesList(streamConfig, targetAddress); + if (attributeList == NULL) { + return NULL; + } + + payload = malloc(MAX_SDP_HEADER_LEN + MAX_SDP_TAIL_LEN + + getSerializedAttributeListSize(attributeList)); + if (payload == NULL) { + freeAttributeList(attributeList); + return NULL; + } + + offset = fillSdpHeader(payload, targetAddress); + offset += fillSerializedAttributeList(payload[offset], attributeList); + offset += fillSdpTail(payload); + + *length = offset; + return payload; +} \ No newline at end of file diff --git a/limelight-common/limelight-common.vcxproj b/limelight-common/limelight-common.vcxproj index c58bb07..bcc43a2 100644 --- a/limelight-common/limelight-common.vcxproj +++ b/limelight-common/limelight-common.vcxproj @@ -135,6 +135,7 @@ + diff --git a/limelight-common/limelight-common.vcxproj.filters b/limelight-common/limelight-common.vcxproj.filters index 3620481..90f998a 100644 --- a/limelight-common/limelight-common.vcxproj.filters +++ b/limelight-common/limelight-common.vcxproj.filters @@ -45,6 +45,9 @@ Source Files + + Source Files +