From fb60ae6a4d5953ff8c1269e2cfef788ca584b6be Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Tue, 31 Mar 2020 20:52:38 -0700 Subject: [PATCH] Parse Opus parameters from RTSP DESCRIBE response --- src/AudioStream.c | 50 +++---------- src/Connection.c | 2 + src/Limelight-internal.h | 2 + src/RtspConnection.c | 155 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 161 insertions(+), 48 deletions(-) diff --git a/src/AudioStream.c b/src/AudioStream.c index 90cfdfa..a2ad0f8 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -26,32 +26,6 @@ static int receivedDataFromPeer; // for longer than normal. #define RTP_RECV_BUFFER (64 * 1024) -#define SAMPLE_RATE 48000 - -static OPUS_MULTISTREAM_CONFIGURATION opusStereoConfig = { - .sampleRate = SAMPLE_RATE, - .channelCount = 2, - .streams = 1, - .coupledStreams = 1, - .mapping = {0, 1} -}; - -static OPUS_MULTISTREAM_CONFIGURATION opus51SurroundConfig = { - .sampleRate = SAMPLE_RATE, - .channelCount = 6, - .streams = 4, - .coupledStreams = 2, - .mapping = {0, 4, 1, 5, 2, 3} -}; - -static OPUS_MULTISTREAM_CONFIGURATION opus51HighSurroundConfig = { - .sampleRate = SAMPLE_RATE, - .channelCount = 6, - .streams = 6, - .coupledStreams = 0, - .mapping = {0, 1, 2, 3, 4, 5} -}; - typedef struct _QUEUED_AUDIO_PACKET { // data must remain at the front char data[MAX_PACKET_SIZE]; @@ -321,22 +295,16 @@ int startAudioStream(void* audioContext, int arFlags) { int err; OPUS_MULTISTREAM_CONFIGURATION chosenConfig; - // TODO: Get these from RTSP ANNOUNCE surround-params - if (StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_STEREO) { - chosenConfig = opusStereoConfig; - } - else if (StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) { - if (HighQualitySurroundEnabled) { - LC_ASSERT(HighQualitySurroundSupported); - chosenConfig = opus51HighSurroundConfig; - } - else { - chosenConfig = opus51SurroundConfig; - } + if (HighQualitySurroundEnabled) { + LC_ASSERT(HighQualitySurroundSupported); + LC_ASSERT(HighQualityOpusConfig.channelCount != 0); + LC_ASSERT(HighQualityOpusConfig.streams != 0); + chosenConfig = HighQualityOpusConfig; } else { - Limelog("Invalid audio configuration: %d\n", StreamConfig.audioConfiguration); - return -1; + LC_ASSERT(NormalQualityOpusConfig.channelCount != 0); + LC_ASSERT(NormalQualityOpusConfig.streams != 0); + chosenConfig = NormalQualityOpusConfig; } chosenConfig.samplesPerFrame = 48 * AudioPacketDuration; @@ -411,4 +379,4 @@ int LiGetPendingAudioFrames(void) { int LiGetPendingAudioDuration(void) { return LiGetPendingAudioFrames() * AudioPacketDuration; -} \ No newline at end of file +} diff --git a/src/Connection.c b/src/Connection.c index 3420b32..5dab9ed 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -20,6 +20,8 @@ int NegotiatedVideoFormat; volatile int ConnectionInterrupted; int HighQualitySurroundSupported; int HighQualitySurroundEnabled; +OPUS_MULTISTREAM_CONFIGURATION NormalQualityOpusConfig; +OPUS_MULTISTREAM_CONFIGURATION HighQualityOpusConfig; int OriginalVideoBitrate; int AudioPacketDuration; diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index 90e6d71..8e32669 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -22,6 +22,8 @@ extern int NegotiatedVideoFormat; extern volatile int ConnectionInterrupted; extern int HighQualitySurroundSupported; extern int HighQualitySurroundEnabled; +extern OPUS_MULTISTREAM_CONFIGURATION NormalQualityOpusConfig; +extern OPUS_MULTISTREAM_CONFIGURATION HighQualityOpusConfig; extern int OriginalVideoBitrate; extern int AudioPacketDuration; diff --git a/src/RtspConnection.c b/src/RtspConnection.c index 6c9aa91..4943f90 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -19,6 +19,16 @@ 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') + +#define SWAP_CHANNEL(config, i, j) \ +{ \ + unsigned char tmp = (config)->mapping[i]; \ + (config)->mapping[i] = (config)->mapping[j]; \ + (config)->mapping[j] = tmp; \ +} + // Create RTSP Option static POPTION_ITEM createOptionItem(char* option, char* content) { @@ -435,6 +445,140 @@ static int sendVideoAnnounce(PRTSP_MESSAGE response, int* error) { 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; +} + +// Parses the Opus configuration from an RTSP DESCRIBE response +static int parseOpusConfigurations(PRTSP_MESSAGE response) { + 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 (StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_STEREO) { + 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; + + LC_ASSERT(StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND); + channelCount = 6; + + // Find the correct audio parameter value + sprintf(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 LFE, but we use FL FR C LFE RL RR. We'll need + // to swap the mappings to match the expected values. + if (channelCount == 6) { + SWAP_CHANNEL(&NormalQualityOpusConfig, 3, 4); + SWAP_CHANNEL(&NormalQualityOpusConfig, 3, 5); + } + + // 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 = 1; + } + } + 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. + if (StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) { + 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; + + HighQualityOpusConfig.channelCount = 6; + HighQualityOpusConfig.streams = 6; + HighQualityOpusConfig.coupledStreams = 0; + HighQualityOpusConfig.mapping[0] = 0; + HighQualityOpusConfig.mapping[1] = 1; + HighQualityOpusConfig.mapping[2] = 2; + HighQualityOpusConfig.mapping[3] = 3; + HighQualityOpusConfig.mapping[4] = 4; + HighQualityOpusConfig.mapping[5] = 5; + } + else { + // We don't have a hardcoded fallback mapping, so we have no choice but to fail. + return -4; + } + } + } + + return 0; +} + // Perform RTSP Handshake with the streaming server machine as part of the connection process int performRtspHandshake(void) { int ret; @@ -575,13 +719,10 @@ int performRtspHandshake(void) { NegotiatedVideoFormat = VIDEO_FORMAT_H264; } - // Check if high bitrate surround sound is available before attempting to request it. - // TODO: Parse these surround-params so we can get rid of our hardcoded Opus mappings - if (strstr(response.payload, "surround-params=660")) { - HighQualitySurroundSupported = 1; - } - else { - HighQualitySurroundSupported = 0; + // Parse the Opus surround parameters out of the RTSP DESCRIBE response. + ret = parseOpusConfigurations(&response); + if (ret != 0) { + goto Exit; } freeMessage(&response);