#include "Limelight-internal.h"

#define MAX_OPTION_NAME_LEN 128

#define MAX_SDP_HEADER_LEN 128
#define MAX_SDP_TAIL_LEN 128

typedef struct _SDP_OPTION {
    char name[MAX_OPTION_NAME_LEN + 1];
    void* payload;
    int payloadLen;
    struct _SDP_OPTION* next;
} SDP_OPTION, *PSDP_OPTION;

// Cleanup the attribute list
static void freeAttributeList(PSDP_OPTION head) {
    PSDP_OPTION next;
    while (head != NULL) {
        next = head->next;
        free(head);
        head = next;
    }
}

// Get the size of the attribute list
static int getSerializedAttributeListSize(PSDP_OPTION head) {
    PSDP_OPTION currentEntry = head;
    size_t size = 0;
    while (currentEntry != NULL) {
        size += strlen("a=");
        size += strlen(currentEntry->name);
        size += strlen(":");
        size += currentEntry->payloadLen;
        size += strlen(" \r\n");

        currentEntry = currentEntry->next;
    }
    // Add one for the null terminator
    return (int)size + 1;
}

// Populate the serialized attribute list into a string
static int fillSerializedAttributeList(char* buffer, size_t length, PSDP_OPTION head) {
    PSDP_OPTION currentEntry = head;
    int offset = 0;
    while (currentEntry != NULL) {
        int ret = snprintf(&buffer[offset], length, "a=%s:", currentEntry->name);
        if (ret > 0 && (size_t)ret < length) {
            offset += ret;
            length -= ret;
        }
        else {
            LC_ASSERT(false);
            break;
        }

        if ((size_t)currentEntry->payloadLen < length) {
            memcpy(&buffer[offset], currentEntry->payload, currentEntry->payloadLen);
            offset += currentEntry->payloadLen;
            length -= currentEntry->payloadLen;
        }
        else {
            LC_ASSERT(false);
            break;
        }

        ret = snprintf(&buffer[offset], length, " \r\n");
        if (ret > 0 && (size_t)ret < length) {
            offset += ret;
            length -= ret;
        }
        else {
            LC_ASSERT(false);
            break;
        }

        currentEntry = currentEntry->next;
    }

    // We should have only space for the null terminator left over
    LC_ASSERT(length == 1);
    return offset;
}

// Add an attribute
static int addAttributeBinary(PSDP_OPTION* head, char* name, const void* payload, int payloadLen) {
    PSDP_OPTION option, currentOption;

    option = malloc(sizeof(*option) + payloadLen);
    if (option == NULL) {
        return -1;
    }

    if (!PltSafeStrcpy(option->name, sizeof(option->name), name)) {
        free(option);
        return -1;
    }

    option->next = NULL;
    option->payloadLen = payloadLen;
    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;
}

// Add an attribute string
static int addAttributeString(PSDP_OPTION* head, char* name, const char* payload) {
    // We purposefully omit the null terminating character
    return addAttributeBinary(head, name, payload, (int)strlen(payload));
}

static int addGen3Options(PSDP_OPTION* head, char* addrStr) {
    int payloadInt;
    int err = 0;

    err |= addAttributeString(head, "x-nv-general.serverAddress", addrStr);

    payloadInt = htonl(0x42774141);
    err |= addAttributeBinary(head,
        "x-nv-general.featureFlags", &payloadInt, sizeof(payloadInt));

    payloadInt = htonl(0x41514141);
    err |= addAttributeBinary(head,
        "x-nv-video[0].transferProtocol", &payloadInt, sizeof(payloadInt));
    err |= addAttributeBinary(head,
        "x-nv-video[1].transferProtocol", &payloadInt, sizeof(payloadInt));
    err |= addAttributeBinary(head,
        "x-nv-video[2].transferProtocol", &payloadInt, sizeof(payloadInt));
    err |= addAttributeBinary(head,
        "x-nv-video[3].transferProtocol", &payloadInt, sizeof(payloadInt));

    payloadInt = htonl(0x42414141);
    err |= addAttributeBinary(head,
        "x-nv-video[0].rateControlMode", &payloadInt, sizeof(payloadInt));
    payloadInt = htonl(0x42514141);
    err |= addAttributeBinary(head,
        "x-nv-video[1].rateControlMode", &payloadInt, sizeof(payloadInt));
    err |= addAttributeBinary(head,
        "x-nv-video[2].rateControlMode", &payloadInt, sizeof(payloadInt));
    err |= addAttributeBinary(head,
        "x-nv-video[3].rateControlMode", &payloadInt, sizeof(payloadInt));

    err |= addAttributeString(head, "x-nv-vqos[0].bw.flags", "14083");

    err |= addAttributeString(head, "x-nv-vqos[0].videoQosMaxConsecutiveDrops", "0");
    err |= addAttributeString(head, "x-nv-vqos[1].videoQosMaxConsecutiveDrops", "0");
    err |= addAttributeString(head, "x-nv-vqos[2].videoQosMaxConsecutiveDrops", "0");
    err |= addAttributeString(head, "x-nv-vqos[3].videoQosMaxConsecutiveDrops", "0");

    return err;
}

static int addGen4Options(PSDP_OPTION* head, char* addrStr) {
    char payloadStr[92];
    int err = 0;

    LC_ASSERT(RtspPortNumber != 0);
    snprintf(payloadStr, sizeof(payloadStr), "rtsp://%s:%u", addrStr, RtspPortNumber);
    err |= addAttributeString(head, "x-nv-general.serverAddress", payloadStr);

    return err;
}

#define NVFF_BASE             0x07
#define NVFF_AUDIO_ENCRYPTION 0x20
#define NVFF_RI_ENCRYPTION    0x80

static int addGen5Options(PSDP_OPTION* head) {
    int err = 0;
    char payloadStr[32];

    // This must be initialized to false already
    LC_ASSERT(!AudioEncryptionEnabled);

    if (APP_VERSION_AT_LEAST(7, 1, 431)) {
        unsigned int featureFlags;

        // RI encryption is always enabled
        featureFlags = NVFF_BASE | NVFF_RI_ENCRYPTION;

        // Enable audio encryption if the client opted in or the host required it
        if ((StreamConfig.encryptionFlags & ENCFLG_AUDIO) || (EncryptionFeaturesEnabled & SS_ENC_AUDIO)) {
            featureFlags |= NVFF_AUDIO_ENCRYPTION;
            AudioEncryptionEnabled = true;
        }

        snprintf(payloadStr, sizeof(payloadStr), "%u", featureFlags);
        err |= addAttributeString(head, "x-nv-general.featureFlags", payloadStr);

        // Ask for the encrypted control protocol to ensure remote input will be encrypted.
        // This used to be done via separate RI encryption, but now it is all or nothing.
        err |= addAttributeString(head, "x-nv-general.useReliableUdp", "13");

        // Require at least 2 FEC packets for small frames. If a frame has fewer data shards
        // than would generate 2 FEC shards, it will increase the FEC percentage for that frame
        // above the set value (even going as high as 200% FEC to generate 2 FEC shards from a
        // 1 data shard frame).
        err |= addAttributeString(head, "x-nv-vqos[0].fec.minRequiredFecPackets", "2");

        // BLL-FEC appears to adjust dynamically based on the loss rate and instantaneous bitrate
        // of each frame, however we can't dynamically control it from our side yet. As a result,
        // the effective FEC amount is significantly lower (single digit percentages for many
        // large frames) and the result is worse performance during packet loss. Disabling BLL-FEC
        // results in GFE 3.26 falling back to the legacy FEC method as we would like.
        err |= addAttributeString(head, "x-nv-vqos[0].bllFec.enable", "0");
    }
    else {
        // We want to use the new ENet connections for control and input
        err |= addAttributeString(head, "x-nv-general.useReliableUdp", "1");
        err |= addAttributeString(head, "x-nv-ri.useControlChannel", "1");

        // When streaming 4K, lower FEC levels to reduce stream overhead
        if (StreamConfig.width >= 3840 && StreamConfig.height >= 2160) {
            err |= addAttributeString(head, "x-nv-vqos[0].fec.repairPercent", "5");
        }
        else {
            err |= addAttributeString(head, "x-nv-vqos[0].fec.repairPercent", "20");
        }
    }
    
    if (APP_VERSION_AT_LEAST(7, 1, 446) && (StreamConfig.width < 720 || StreamConfig.height < 540)) {
        // We enable DRC with a static DRC table for very low resoutions on GFE 3.26 to work around
        // a bug that causes nvstreamer.exe to crash due to failing to populate a list of valid resolutions.
        //
        // Despite the fact that the DRC table doesn't include our target streaming resolution, we still
        // seem to stream at the target resolution, presumably because we don't send control data to tell
        // the host otherwise.
        err |= addAttributeString(head, "x-nv-vqos[0].drc.enable", "1");
        err |= addAttributeString(head, "x-nv-vqos[0].drc.tableType", "2");
    }
    else {
        // Disable dynamic resolution switching
        err |= addAttributeString(head, "x-nv-vqos[0].drc.enable", "0");
    }

    // Recovery mode can cause the FEC percentage to change mid-frame, which
    // breaks many assumptions in RTP FEC queue.
    err |= addAttributeString(head, "x-nv-general.enableRecoveryMode", "0");

    return err;
}

static PSDP_OPTION getAttributesList(char*urlSafeAddr) {
    PSDP_OPTION optionHead;
    char payloadStr[92];
    int audioChannelCount;
    int audioChannelMask;
    int err;
    int adjustedBitrate;

    // This must have been resolved to either local or remote by now
    LC_ASSERT(StreamConfig.streamingRemotely != STREAM_CFG_AUTO);

    optionHead = NULL;
    err = 0;

    if (IS_SUNSHINE()) {
        // Send client feature flags to Sunshine hosts
        uint32_t moonlightFeatureFlags = ML_FF_FEC_STATUS | ML_FF_SESSION_ID_V1;
        snprintf(payloadStr, sizeof(payloadStr), "%u", moonlightFeatureFlags);
        err |= addAttributeString(&optionHead, "x-ml-general.featureFlags", payloadStr);

        // New-style control stream encryption is low overhead, so we enable it any time it is supported
        if (EncryptionFeaturesSupported & SS_ENC_CONTROL_V2) {
            EncryptionFeaturesEnabled |= SS_ENC_CONTROL_V2;
        }

        // If video encryption is supported by the host and desired by the client, use it
        if ((EncryptionFeaturesSupported & SS_ENC_VIDEO) && (StreamConfig.encryptionFlags & ENCFLG_VIDEO)) {
            EncryptionFeaturesEnabled |= SS_ENC_VIDEO;
        }
        else if ((EncryptionFeaturesRequested & SS_ENC_VIDEO) && !(StreamConfig.encryptionFlags & ENCFLG_VIDEO)) {
            // If video encryption is explicitly requested by the host but *not* by the client,
            // we'll encrypt anyway (since we are capable of doing so) and print a warning.
            Limelog("Enabling video encryption by host request despite client opt-out. Performance may suffer!");
            EncryptionFeaturesEnabled |= SS_ENC_VIDEO;
        }

        // If audio encryption is supported by the host and desired by the client, use it
        if ((EncryptionFeaturesSupported & SS_ENC_AUDIO) && (StreamConfig.encryptionFlags & ENCFLG_AUDIO)) {
            EncryptionFeaturesEnabled |= SS_ENC_AUDIO;
        }
        else if ((EncryptionFeaturesRequested & SS_ENC_AUDIO) && !(StreamConfig.encryptionFlags & ENCFLG_AUDIO)) {
            // If audio encryption is explicitly requested by the host but *not* by the client,
            // we'll encrypt anyway (since we are capable of doing so) and print a warning.
            Limelog("Enabling audio encryption by host request despite client opt-out. Audio quality may suffer!");
            EncryptionFeaturesEnabled |= SS_ENC_AUDIO;
        }

        snprintf(payloadStr, sizeof(payloadStr), "%u", EncryptionFeaturesEnabled);
        err |= addAttributeString(&optionHead, "x-ss-general.encryptionEnabled", payloadStr);
    }

    snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.width);
    err |= addAttributeString(&optionHead, "x-nv-video[0].clientViewportWd", payloadStr);
    snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.height);
    err |= addAttributeString(&optionHead, "x-nv-video[0].clientViewportHt", payloadStr);

    snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.fps);
    err |= addAttributeString(&optionHead, "x-nv-video[0].maxFPS", payloadStr);

    // Adjust the video packet size to account for encryption overhead
    if (EncryptionFeaturesEnabled & SS_ENC_VIDEO) {
        LC_ASSERT(StreamConfig.packetSize % 16 == 0);
        StreamConfig.packetSize -= sizeof(ENC_VIDEO_HEADER);
        LC_ASSERT(StreamConfig.packetSize % 16 == 0);
    }
    snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.packetSize);
    err |= addAttributeString(&optionHead, "x-nv-video[0].packetSize", payloadStr);

    err |= addAttributeString(&optionHead, "x-nv-video[0].rateControlMode", "4");

    err |= addAttributeString(&optionHead, "x-nv-video[0].timeoutLengthMs", "7000");
    err |= addAttributeString(&optionHead, "x-nv-video[0].framesWithInvalidRefThreshold", "0");

    // 20% of the video bitrate will added to the user-specified bitrate for FEC
    adjustedBitrate = (int)(StreamConfig.bitrate * 0.80);

    // Use more strict bitrate logic when streaming remotely. The theory here is that remote
    // streaming is much more bandwidth sensitive. Someone might select 5 Mbps because that's
    // really all they have, so we need to be careful not to exceed the cap, even counting
    // things like audio and control data.
    if (StreamConfig.streamingRemotely == STREAM_CFG_REMOTE) {
        // Subtract 500 Kbps to leave room for audio and control. On remote streams,
        // GFE will use 96Kbps stereo audio. For local streams, it will choose 512Kbps.
        if (adjustedBitrate > 500) {
            adjustedBitrate -= 500;
        }
    }

    // GFE currently imposes a limit of 100 Mbps for the video bitrate. It will automatically
    // impose that on maximumBitrateKbps but not on initialBitrateKbps. We will impose the cap
    // ourselves so initialBitrateKbps does not exceed maximumBitrateKbps.
    adjustedBitrate = adjustedBitrate > 100000 ? 100000 : adjustedBitrate;

    // We don't support dynamic bitrate scaling properly (it tends to bounce between min and max and never
    // settle on the optimal bitrate if it's somewhere in the middle), so we'll just latch the bitrate
    // to the requested value.
    if (AppVersionQuad[0] >= 5) {
        snprintf(payloadStr, sizeof(payloadStr), "%d", adjustedBitrate);

        err |= addAttributeString(&optionHead, "x-nv-video[0].initialBitrateKbps", payloadStr);
        err |= addAttributeString(&optionHead, "x-nv-video[0].initialPeakBitrateKbps", payloadStr);

        err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.minimumBitrateKbps", payloadStr);
        err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.maximumBitrateKbps", payloadStr);

        // Send the configured bitrate to Sunshine hosts, so they can adjust for dynamic FEC percentage
        if (IS_SUNSHINE()) {
            snprintf(payloadStr, sizeof(payloadStr), "%u", StreamConfig.bitrate);
            err |= addAttributeString(&optionHead, "x-ml-video.configuredBitrateKbps", payloadStr);
        }
    }
    else {
        if (StreamConfig.streamingRemotely == STREAM_CFG_REMOTE) {
            err |= addAttributeString(&optionHead, "x-nv-video[0].averageBitrate", "4");
            err |= addAttributeString(&optionHead, "x-nv-video[0].peakBitrate", "4");
        }

        snprintf(payloadStr, sizeof(payloadStr), "%d", adjustedBitrate);
        err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.minimumBitrate", payloadStr);
        err |= addAttributeString(&optionHead, "x-nv-vqos[0].bw.maximumBitrate", payloadStr);
    }
    
    // FEC must be enabled for proper packet sequencing to be done by RTP FEC queue
    err |= addAttributeString(&optionHead, "x-nv-vqos[0].fec.enable", "1");
    
    err |= addAttributeString(&optionHead, "x-nv-vqos[0].videoQualityScoreUpdateTime", "5000");

    // If the remote host is local (RFC 1918), enable QoS tagging for our traffic. Windows qWave
    // will disable it if the host is off-link, *however* Windows may get it wrong in cases where
    // the host is directly connected to the Internet without a NAT. In this case, it may send DSCP
    // marked traffic off-link and it could lead to black holes due to misconfigured ISP hardware
    // or CPE. For this reason, we only enable it in cases where it looks like it will work.
    //
    // Even though IPv6 hardware should be much less likely to have this issue, we can't tell
    // if our address is a NAT64 synthesized IPv6 address or true end-to-end IPv6. If it's the
    // former, it may have the same problem as other IPv4 traffic.
    if (StreamConfig.streamingRemotely == STREAM_CFG_LOCAL) {
        err |= addAttributeString(&optionHead, "x-nv-vqos[0].qosTrafficType", "5");
        err |= addAttributeString(&optionHead, "x-nv-aqos.qosTrafficType", "4");
    }
    else {
        err |= addAttributeString(&optionHead, "x-nv-vqos[0].qosTrafficType", "0");
        err |= addAttributeString(&optionHead, "x-nv-aqos.qosTrafficType", "0");
    }

    if (AppVersionQuad[0] == 3) {
        err |= addGen3Options(&optionHead, urlSafeAddr);
    }
    else if (AppVersionQuad[0] == 4) {
        err |= addGen4Options(&optionHead, urlSafeAddr);
    }
    else {
        err |= addGen5Options(&optionHead);
    }

    audioChannelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration);
    audioChannelMask = CHANNEL_MASK_FROM_AUDIO_CONFIGURATION(StreamConfig.audioConfiguration);

    if (AppVersionQuad[0] >= 4) {
        unsigned char slicesPerFrame;

        // Use slicing for increased performance on some decoders
        slicesPerFrame = (unsigned char)(VideoCallbacks.capabilities >> 24);
        if (slicesPerFrame == 0) {
            // If not using slicing, we request 1 slice per frame
            slicesPerFrame = 1;
        }
        snprintf(payloadStr, sizeof(payloadStr), "%d", slicesPerFrame);
        err |= addAttributeString(&optionHead, "x-nv-video[0].videoEncoderSlicesPerFrame", payloadStr);

        if (NegotiatedVideoFormat & VIDEO_FORMAT_MASK_AV1) {
            err |= addAttributeString(&optionHead, "x-nv-vqos[0].bitStreamFormat", "2");
        }
        else if (NegotiatedVideoFormat & VIDEO_FORMAT_MASK_H265) {
            err |= addAttributeString(&optionHead, "x-nv-clientSupportHevc", "1");
            err |= addAttributeString(&optionHead, "x-nv-vqos[0].bitStreamFormat", "1");

            if (!APP_VERSION_AT_LEAST(7, 1, 408)) {
                // This disables split frame encode on GFE 3.10 which seems to produce broken
                // HEVC output at 1080p60 (full of artifacts even on the SHIELD itself, go figure).
                // It now appears to work fine on GFE 3.14.1.
                Limelog("Disabling split encode for HEVC on older GFE version");
                err |= addAttributeString(&optionHead, "x-nv-video[0].encoderFeatureSetting", "0");
            }
        }
        else {
            err |= addAttributeString(&optionHead, "x-nv-clientSupportHevc", "0");
            err |= addAttributeString(&optionHead, "x-nv-vqos[0].bitStreamFormat", "0");
        }

        if (AppVersionQuad[0] >= 7) {
            // Enable HDR if requested
            if (NegotiatedVideoFormat & VIDEO_FORMAT_MASK_10BIT) {
                err |= addAttributeString(&optionHead, "x-nv-video[0].dynamicRangeMode", "1");
            }
            else {
                err |= addAttributeString(&optionHead, "x-nv-video[0].dynamicRangeMode", "0");
            }

            // If the decoder supports reference frame invalidation, that indicates it also supports
            // the maximum number of reference frames allowed by the codec. Even if we can't use RFI
            // due to lack of host support, we can still allow the host to pick a number of reference
            // frames greater than 1 to improve encoding efficiency.
            if (isReferenceFrameInvalidationSupportedByDecoder()) {
                err |= addAttributeString(&optionHead, "x-nv-video[0].maxNumReferenceFrames", "0");
            }
            else {
                // Restrict the video stream to 1 reference frame if we're not using
                // reference frame invalidation. This helps to improve compatibility with
                // some decoders that don't like the default of having 16 reference frames.
                err |= addAttributeString(&optionHead, "x-nv-video[0].maxNumReferenceFrames", "1");
            }

            snprintf(payloadStr, sizeof(payloadStr), "%d", StreamConfig.clientRefreshRateX100);
            err |= addAttributeString(&optionHead, "x-nv-video[0].clientRefreshRateX100", payloadStr);
        }

        snprintf(payloadStr, sizeof(payloadStr), "%d", audioChannelCount);
        err |= addAttributeString(&optionHead, "x-nv-audio.surround.numChannels", payloadStr);
        snprintf(payloadStr, sizeof(payloadStr), "%d", audioChannelMask);
        err |= addAttributeString(&optionHead, "x-nv-audio.surround.channelMask", payloadStr);
        if (audioChannelCount > 2) {
            err |= addAttributeString(&optionHead, "x-nv-audio.surround.enable", "1");
        }
        else {
            err |= addAttributeString(&optionHead, "x-nv-audio.surround.enable", "0");
        }
    }

    if (AppVersionQuad[0] >= 7) {
        if (StreamConfig.bitrate >= HIGH_AUDIO_BITRATE_THRESHOLD && audioChannelCount > 2 &&
                HighQualitySurroundSupported && (AudioCallbacks.capabilities & CAPABILITY_SLOW_OPUS_DECODER) == 0) {
            // Enable high quality mode for surround sound
            err |= addAttributeString(&optionHead, "x-nv-audio.surround.AudioQuality", "1");

            // Let the audio stream code know that it needs to disable coupled streams when
            // decoding this audio stream.
            HighQualitySurroundEnabled = true;

            // Use 5 ms frames since we don't have a slow decoder
            AudioPacketDuration = 5;
        }
        else {
            err |= addAttributeString(&optionHead, "x-nv-audio.surround.AudioQuality", "0");
            HighQualitySurroundEnabled = false;

            if ((AudioCallbacks.capabilities & CAPABILITY_SLOW_OPUS_DECODER) ||
                     ((AudioCallbacks.capabilities & CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION) != 0 &&
                       StreamConfig.bitrate < LOW_AUDIO_BITRATE_TRESHOLD)) {
                // Use 10 ms packets for slow devices and networks to balance latency and bandwidth usage
                AudioPacketDuration = 10;
            }
            else {
                // Use 5 ms packets by default for lowest latency
                AudioPacketDuration = 5;
            }
        }

        snprintf(payloadStr, sizeof(payloadStr), "%d", AudioPacketDuration);
        err |= addAttributeString(&optionHead, "x-nv-aqos.packetDuration", payloadStr);
    }
    else {
        // 5 ms duration for legacy servers
        AudioPacketDuration = 5;

        // High quality audio mode not supported on legacy servers
        HighQualitySurroundEnabled = false;
    }

    if (AppVersionQuad[0] >= 7) {
        snprintf(payloadStr, sizeof(payloadStr), "%d", (StreamConfig.colorSpace << 1) | StreamConfig.colorRange);
        err |= addAttributeString(&optionHead, "x-nv-video[0].encoderCscMode", payloadStr);
    }

    if (err == 0) {
        return optionHead;
    }

    freeAttributeList(optionHead);
    return NULL;
}

// Populate the SDP header with required information
static int fillSdpHeader(char* buffer, size_t length, int rtspClientVersion, char*urlSafeAddr) {
    return snprintf(buffer, length,
        "v=0\r\n"
        "o=android 0 %d IN %s %s\r\n"
        "s=NVIDIA Streaming Client\r\n",
        rtspClientVersion,
        RemoteAddr.ss_family == AF_INET ? "IPv4" : "IPv6",
        urlSafeAddr);
}

// Populate the SDP tail with required information
static int fillSdpTail(char* buffer, size_t length) {
    LC_ASSERT(VideoPortNumber != 0);
    return snprintf(buffer, length,
        "t=0 0\r\n"
        "m=video %d  \r\n",
        AppVersionQuad[0] < 4 ? 47996 : VideoPortNumber);
}

// Get the SDP attributes for the stream config
char* getSdpPayloadForStreamConfig(int rtspClientVersion, int* length) {
    PSDP_OPTION attributeList;
    int attributeListSize;
    int offset, written;
    char* payload;
    char urlSafeAddr[URLSAFESTRING_LEN];

    addrToUrlSafeString(&RemoteAddr, urlSafeAddr, sizeof(urlSafeAddr));

    attributeList = getAttributesList(urlSafeAddr);
    if (attributeList == NULL) {
        return NULL;
    }

    attributeListSize = getSerializedAttributeListSize(attributeList);
    payload = malloc(MAX_SDP_HEADER_LEN + MAX_SDP_TAIL_LEN + attributeListSize);
    if (payload == NULL) {
        freeAttributeList(attributeList);
        return NULL;
    }

    offset = 0;
    written = fillSdpHeader(payload, MAX_SDP_HEADER_LEN, rtspClientVersion, urlSafeAddr);
    if (written < 0 || written >= MAX_SDP_HEADER_LEN) {
        LC_ASSERT(false);
        free(payload);
        freeAttributeList(attributeList);
        return NULL;
    }
    else {
        offset += written;
    }
    written = fillSerializedAttributeList(&payload[offset], attributeListSize, attributeList);
    if (written < 0 || written >= attributeListSize) {
        LC_ASSERT(false);
        free(payload);
        freeAttributeList(attributeList);
        return NULL;
    }
    else {
        offset += written;
    }
    written = fillSdpTail(&payload[offset], MAX_SDP_TAIL_LEN);
    if (written < 0 || written >= MAX_SDP_TAIL_LEN) {
        LC_ASSERT(false);
        free(payload);
        freeAttributeList(attributeList);
        return NULL;
    }
    else {
        offset += written;
    }

    freeAttributeList(attributeList);
    *length = offset;
    return payload;
}
