From 56ccd99cc77719d774dec5f679b4b74ba1cc632a Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Thu, 1 Jul 2021 23:45:33 -0500 Subject: [PATCH] Dynamically determine audio, video, and control ports from RTSP DESCRIBE response --- src/AudioStream.c | 26 +++++++++++----- src/Connection.c | 10 ++++++ src/ControlStream.c | 5 +-- src/Limelight-internal.h | 6 ++++ src/RtspConnection.c | 67 +++++++++++++++++++++++++++++++++++++--- src/VideoStream.c | 3 +- 6 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/AudioStream.c b/src/AudioStream.c index 72522f9..4cd18f1 100644 --- a/src/AudioStream.c +++ b/src/AudioStream.c @@ -14,6 +14,7 @@ static uint32_t avRiKeyId; static unsigned short lastSeq; +static bool pingThreadStarted; static bool receivedDataFromPeer; static uint64_t firstReceiveTime; @@ -22,8 +23,6 @@ static uint64_t firstReceiveTime; static uint8_t opusHeaderByte; #endif -#define RTP_PORT 48000 - #define MAX_PACKET_SIZE 1400 // This is much larger than we should typically have buffered, but @@ -47,7 +46,7 @@ static void AudioPingThreadProc(void* context) { LC_SOCKADDR saddr; memcpy(&saddr, &RemoteAddr, sizeof(saddr)); - SET_PORT(&saddr, RTP_PORT); + SET_PORT(&saddr, AudioPortNumber); // Send PING every 500 milliseconds while (!PltIsThreadInterrupted(&udpPingThread)) { @@ -74,6 +73,7 @@ int initializeAudioStream(void) { RtpaInitializeQueue(&rtpAudioQueue); lastSeq = 0; receivedDataFromPeer = false; + pingThreadStarted = false; firstReceiveTime = 0; audioDecryptionCtx = PltCreateCryptoContext(); #ifdef LC_DEBUG @@ -91,15 +91,23 @@ int initializeAudioStream(void) { return LastSocketFail(); } + return 0; +} + +// This is called when the RTSP DESCRIBE message is parsed and the audio port +// number is parsed out of it. Alternatively, it's also called if parsing fails +// and will use the well known audio port instead. +int notifyAudioPortNegotiationComplete(void) { + LC_ASSERT(!pingThreadStarted); + // We may receive audio before our threads are started, but that's okay. We'll // drop the first 1 second of audio packets to catch up with the backlog. int err = PltCreateThread("AudioPing", AudioPingThreadProc, NULL, &udpPingThread); if (err != 0) { - closeSocket(rtpSocket); - rtpSocket = INVALID_SOCKET; return err; } + pingThreadStarted = true; return 0; } @@ -119,9 +127,11 @@ static void freePacketList(PLINKED_BLOCKING_QUEUE_ENTRY entry) { // Tear down the audio stream once we're done with it void destroyAudioStream(void) { if (rtpSocket != INVALID_SOCKET) { - PltInterruptThread(&udpPingThread); - PltJoinThread(&udpPingThread); - PltCloseThread(&udpPingThread); + if (pingThreadStarted) { + PltInterruptThread(&udpPingThread); + PltJoinThread(&udpPingThread); + PltCloseThread(&udpPingThread); + } closeSocket(rtpSocket); rtpSocket = INVALID_SOCKET; diff --git a/src/Connection.c b/src/Connection.c index ab5a6f7..d8f3525 100644 --- a/src/Connection.c +++ b/src/Connection.c @@ -24,6 +24,10 @@ OPUS_MULTISTREAM_CONFIGURATION HighQualityOpusConfig; int OriginalVideoBitrate; int AudioPacketDuration; bool AudioEncryptionEnabled; +uint16_t RtspPortNumber; +uint16_t ControlPortNumber; +uint16_t AudioPortNumber; +uint16_t VideoPortNumber; // Connection stages static const char* stageNames[STAGE_MAX] = { @@ -208,6 +212,12 @@ int LiStartConnection(PSERVER_INFORMATION serverInfo, PSTREAM_CONFIGURATION stre OriginalVideoBitrate = streamConfig->bitrate; RemoteAddrString = strdup(serverInfo->address); + // Configure default ports + VideoPortNumber = 47998; + ControlPortNumber = 47999; + AudioPortNumber = 48000; + RtspPortNumber = 48010; // TODO: Parse this out of RTSP session URL + alreadyTerminated = false; ConnectionInterrupted = false; diff --git a/src/ControlStream.c b/src/ControlStream.c index a9cee33..02fe462 100644 --- a/src/ControlStream.c +++ b/src/ControlStream.c @@ -1143,7 +1143,7 @@ int startControlStream(void) { ENetEvent event; enet_address_set_address(&address, (struct sockaddr *)&RemoteAddr, RemoteAddrLen); - enet_address_set_port(&address, 47999); + enet_address_set_port(&address, ControlPortNumber); // 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); @@ -1166,7 +1166,7 @@ int startControlStream(void) { // 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"); + Limelog("Failed to connect to UDP port %u\n", ControlPortNumber); stopping = true; enet_peer_reset(peer); peer = NULL; @@ -1182,6 +1182,7 @@ int startControlStream(void) { enet_peer_timeout(peer, 2, 10000, 10000); } else { + // NB: Do NOT use ControlPortNumber here. 47995 is correct for these old versions. ctlSock = connectTcpSocket(&RemoteAddr, RemoteAddrLen, 47995, CONTROL_STREAM_TIMEOUT_SEC); if (ctlSock == INVALID_SOCKET) { diff --git a/src/Limelight-internal.h b/src/Limelight-internal.h index d177d91..9402a43 100644 --- a/src/Limelight-internal.h +++ b/src/Limelight-internal.h @@ -32,6 +32,11 @@ extern int OriginalVideoBitrate; extern int AudioPacketDuration; extern bool AudioEncryptionEnabled; +extern uint16_t RtspPortNumber; +extern uint16_t ControlPortNumber; +extern uint16_t AudioPortNumber; +extern uint16_t VideoPortNumber; + #ifndef UINT24_MAX #define UINT24_MAX 0xFFFFFF #endif @@ -100,6 +105,7 @@ int startVideoStream(void* rendererContext, int drFlags); void stopVideoStream(void); int initializeAudioStream(void); +int notifyAudioPortNegotiationComplete(void); void destroyAudioStream(void); int startAudioStream(void* audioContext, int arFlags); void stopAudioStream(void); diff --git a/src/RtspConnection.c b/src/RtspConnection.c index 465190f..258f926 100644 --- a/src/RtspConnection.c +++ b/src/RtspConnection.c @@ -228,7 +228,7 @@ static bool transactRtspMessageTcp(PRTSP_MESSAGE request, PRTSP_MESSAGE response // returns HTTP 200 OK for the /launch request before the RTSP handshake port // is listening. do { - sock = connectTcpSocket(&RemoteAddr, RemoteAddrLen, 48010, RTSP_TIMEOUT_SEC); + sock = connectTcpSocket(&RemoteAddr, RemoteAddrLen, RtspPortNumber, RTSP_TIMEOUT_SEC); if (sock == INVALID_SOCKET) { *error = LastSocketError(); if (*error == ECONNREFUSED) { @@ -525,6 +525,57 @@ static int parseOpusConfigFromParamString(char* paramStr, int channelCount, POPU return 0; } +static bool parseMediaEntry(PRTSP_MESSAGE response, const char* mediaType, const char* transport, uint16_t* port) { + char paramsPrefix[128]; + char paramsSuffix[128]; + char* paramStart; + + sprintf(paramsPrefix, "m=%s ", mediaType); + sprintf(paramsSuffix, " %s", transport); + + // Look for the next match + paramStart = response->payload; + while ((paramStart = strstr(paramStart, paramsPrefix)) != NULL) { + // Skip the prefix + paramStart += strlen(paramsPrefix); + + // The first part should be the port number + char* nextToken; + long int rawPort = strtol(paramStart, &nextToken, 10); + if (rawPort <= 0 || rawPort >= 65535) { + continue; + } + + // Skip this entry if the transport isn't what we expect + if (strncmp(nextToken, paramsSuffix, strlen(paramsSuffix)) != 0) { + continue; + } + + // This entry is a match + *port = (uint16_t)rawPort; + return true; + } + + // No match for this media type and transport + return false; +} + +static void parsePortConfigurations(PRTSP_MESSAGE response) { + // Don't parse ports on very old GFE versions + if (!APP_VERSION_AT_LEAST(7, 0, 0)) { + return; + } + + parseMediaEntry(response, "video", "RTP/AVP", &VideoPortNumber); + parseMediaEntry(response, "audio", "RTP/AVP", &AudioPortNumber); + + if (!parseMediaEntry(response, "application", "udp", &ControlPortNumber)) { + if (!parseMediaEntry(response, "application", "udp_enc", &ControlPortNumber)) { + parseMediaEntry(response, "application", "udp_ag", &ControlPortNumber); + } + } +} + // Parses the Opus configuration from an RTSP DESCRIBE response static int parseOpusConfigurations(PRTSP_MESSAGE response) { HighQualitySurroundSupported = false; @@ -644,7 +695,7 @@ int performRtspHandshake(void) { // Initialize global state useEnet = (AppVersionQuad[0] >= 5) && (AppVersionQuad[0] <= 7) && (AppVersionQuad[2] < 404); - sprintf(rtspTargetUrl, "rtsp%s://%s:48010", useEnet ? "ru" : "", urlAddr); + sprintf(rtspTargetUrl, "rtsp%s://%s:%u", useEnet ? "ru" : "", urlAddr, RtspPortNumber); currentSeqNumber = 1; hasSessionId = false; controlStreamId = APP_VERSION_AT_LEAST(7, 1, 431) ? "streamid=control/13/0" : "streamid=control/1/0"; @@ -676,7 +727,7 @@ int performRtspHandshake(void) { ENetEvent event; enet_address_set_address(&address, (struct sockaddr *)&RemoteAddr, RemoteAddrLen); - enet_address_set_port(&address, 48010); + enet_address_set_port(&address, RtspPortNumber); // Create a client that can use 1 outgoing connection and 1 channel client = enet_host_create(RemoteAddr.ss_family, NULL, 1, 1, 0, 0); @@ -695,7 +746,7 @@ int performRtspHandshake(void) { // Wait for the connect to complete if (serviceEnetHost(client, &event, RTSP_TIMEOUT_SEC * 1000) <= 0 || event.type != ENET_EVENT_TYPE_CONNECT) { - Limelog("RTSP: Failed to connect to UDP port 48010\n"); + Limelog("RTSP: Failed to connect to UDP port %u\n", RtspPortNumber); enet_peer_reset(peer); peer = NULL; enet_host_destroy(client); @@ -773,6 +824,14 @@ int performRtspHandshake(void) { } } + // Parse audio, video, and control ports out of the RTSP DESCRIBE response. + parsePortConfigurations(&response); + + // Let the audio stream know the port number is now finalized. + // NB: This is needed because audio stream init happens before RTSP, + // which is not the case for the video stream. + notifyAudioPortNegotiationComplete(); + // Parse the Opus surround parameters out of the RTSP DESCRIBE response. ret = parseOpusConfigurations(&response); if (ret != 0) { diff --git a/src/VideoStream.c b/src/VideoStream.c index c3d9838..8244ad0 100644 --- a/src/VideoStream.c +++ b/src/VideoStream.c @@ -3,7 +3,6 @@ #define FIRST_FRAME_MAX 1500 #define FIRST_FRAME_TIMEOUT_SEC 10 -#define RTP_PORT 47998 #define FIRST_FRAME_PORT 47996 #define RTP_RECV_BUFFER (512 * 1024) @@ -48,7 +47,7 @@ static void VideoPingThreadProc(void* context) { LC_SOCKADDR saddr; memcpy(&saddr, &RemoteAddr, sizeof(saddr)); - SET_PORT(&saddr, RTP_PORT); + SET_PORT(&saddr, VideoPortNumber); while (!PltIsThreadInterrupted(&udpPingThread)) { // We do not check for errors here. Socket errors will be handled