13#include "AudioTools/AudioCodecs/CodecNetworkFormat.h"
14#include "AudioTools/AudioCodecs/MultiDecoder.h"
15#include "AudioTools/CoreAudio//BaseStream.h"
16#include "AudioTools/CoreAudio/AudioBasic/Collections/Vector.h"
17#include "AudioTools/CoreAudio/Buffers.h"
18#include "AudioTools/CoreAudio/ResampleStream.h"
46template <
typename TcpClient,
typename UdpSocket>
51 m_multi_decoder.
addDecoder(m_decoder_net,
"audio/L16");
53 m_multi_decoder.
addDecoder(m_decoder_l8,
"audio/L8");
93 if (factor <= 0.0f) factor = 1.0f;
94 float step = 1.0f / factor;
95 m_resampleStep = step;
137 bool begin(IPAddress addr, uint16_t port,
const char* path =
nullptr) {
142 if (m_tcp.connected()) m_tcp.stop();
143 LOGI(
"RTSPClient: connecting to %u.%u.%u.%u:%u", m_addr[0], m_addr[1],
144 m_addr[2], m_addr[3], (
unsigned)m_port);
146 bool connected =
false;
147 for (uint8_t attempt = 0; attempt <= m_connectRetries; ++attempt) {
148 if (m_tcp.connect(m_addr, m_port)) {
152 LOGW(
"RTSPClient: connect attempt %u failed", (
unsigned)(attempt + 1));
153 if (attempt < m_connectRetries) delay(m_connectRetryDelayMs);
156 LOGE(
"RTSPClient: TCP connect failed");
159 m_tcp.setNoDelay(
true);
169 int retry = m_connectRetries;
170 while (!sendSimpleRequest(
"OPTIONS", m_baseUrl,
nullptr, 0, m_hdrBuf,
171 sizeof(m_hdrBuf),
nullptr, 0)) {
173 return fail(
"OPTIONS failed");
175 LOGW(
"RTSPClient: retrying OPTIONS");
182 const char* describeExtra =
"Accept: application/sdp\r\n";
183 if (!sendSimpleRequest(
"DESCRIBE", m_baseUrl, describeExtra,
184 strlen(describeExtra), m_hdrBuf,
sizeof(m_hdrBuf),
185 m_bodyBuf,
sizeof(m_bodyBuf)))
186 return fail(
"DESCRIBE failed");
191 parseContentBaseFromHeaders(m_hdrBuf);
193 parseControlFromSdp(m_bodyBuf);
194 buildTrackUrlFromBaseAndControl();
195 LOGI(
"RTSPClient: SDP control='%s' content-base='%s'", m_sdpControl,
197 LOGI(
"RTSPClient: SETUP url: %s", m_trackUrl);
200 if (!openUdpPorts())
return fail(
"UDP bind failed");
203 char transportHdr[128];
204 snprintf(transportHdr,
sizeof(transportHdr),
205 "Transport: RTP/AVP;unicast;client_port=%u-%u\r\n",
206 (
unsigned)m_clientRtpPort, (
unsigned)(m_clientRtpPort + 1));
207 if (!sendSimpleRequest(
"SETUP", m_trackUrl, transportHdr,
208 strlen(transportHdr), m_hdrBuf,
sizeof(m_hdrBuf),
211 snprintf(transportHdr,
sizeof(transportHdr),
212 "Transport: RTP/AVP/UDP;unicast;client_port=%u-%u\r\n",
213 (
unsigned)m_clientRtpPort, (
unsigned)(m_clientRtpPort + 1));
214 if (!sendSimpleRequest(
"SETUP", m_trackUrl, transportHdr,
215 strlen(transportHdr), m_hdrBuf,
sizeof(m_hdrBuf),
217 return fail(
"SETUP failed");
222 parseSessionFromHeaders(m_hdrBuf);
223 parseServerPortsFromHeaders(m_hdrBuf);
224 if (m_sessionId[0] ==
'\0')
return fail(
"Missing Session ID");
231 char sessionHdr[128];
232 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n", m_sessionId);
233 if (!sendSimpleRequest(
"PLAY", m_baseUrl, sessionHdr, strlen(sessionHdr),
234 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0)) {
238 if (sniffUdpFor(1500)) {
239 LOGW(
"RTSPClient: proceeding without PLAY response (RTP detected)");
241 return fail(
"PLAY failed");
247 m_lastKeepaliveMs =
millis();
253 operator bool() {
return m_started &&
mime() !=
nullptr &&
available() > 0; }
261 if (m_tcp.connected()) {
262 char sessionHdr[128];
263 if (m_sessionId[0]) {
264 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n",
266 sendSimpleRequest(
"TEARDOWN", m_baseUrl, sessionHdr,
267 strlen(sessionHdr), m_hdrBuf,
sizeof(m_hdrBuf),
276 if (m_tcp.connected()) m_tcp.stop();
286 delay(m_idleDelayMs);
292 delay(m_idleDelayMs);
297 if (avail == 0) delay(m_idleDelayMs);
307 switch (m_payloadType) {
318 return "audio/adpcm";
332 if (strcasecmp(m_encoding,
"L16") == 0)
return "audio/L16";
333 if (strcasecmp(m_encoding,
"L8") == 0)
return "audio/L8";
334 if (strcasecmp(m_encoding,
"PCMU") == 0)
return "audio/PCMU";
335 if (strcasecmp(m_encoding,
"PCMA") == 0)
return "audio/PCMA";
336 if (strcasecmp(m_encoding,
"GSM") == 0)
return "audio/gsm";
337 if (strcasecmp(m_encoding,
"MPA") == 0)
return "audio/mpeg";
338 if (strcasecmp(m_encoding,
"MPEG4-GENERIC") == 0)
return "audio/aac";
339 if (strcasecmp(m_encoding,
"OPUS") == 0)
return "audio/opus";
340 if (strcasecmp(m_encoding,
"DVI4") == 0)
return "audio/adpcm";
355 if (!m_started || !m_tcp.connected() || m_sessionId[0] ==
'\0')
357 if (active == m_isPlaying)
return true;
359 char sessionHdr[128];
360 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n", m_sessionId);
363 ok = sendSimpleRequest(
"PLAY", m_baseUrl, sessionHdr, strlen(sessionHdr),
364 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0);
365 if (ok) m_isPlaying =
true;
367 ok = sendSimpleRequest(
"PAUSE", m_baseUrl, sessionHdr, strlen(sessionHdr),
368 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0);
384 m_multi_decoder.
addDecoder(decoder, mimeType);
394 delay(m_idleDelayMs);
402 delay(m_idleDelayMs);
409 if (m_pktBuf.isEmpty()) {
411 delay(m_idleDelayMs);
416 if (!m_decoderReady) {
417 const char* m =
mime();
419 LOGI(
"Selecting decoder: %s", m);
423 if (m_multi_decoder.getOutput() !=
nullptr) {
424 m_multi_decoder.
begin();
426 m_decoderReady =
true;
431 size_t written = m_multi_decoder.
write(m_pktBuf.
data(), n);
432 LOGI(
"copy: %d -> %d", (
int)n, (
int)written);
457 void setNotifyActive(
bool flag) { m_multi_decoder.
setNotifyActive(flag); }
458 bool isNotifyActive() {
return m_multi_decoder.
isNotifyActive(); }
464 bool m_udp_active =
false;
470 char m_baseUrl[96] = {0};
471 char m_trackUrl[128] = {0};
472 char m_contentBase[160] = {0};
473 char m_sdpControl[128] = {0};
474 char m_sessionId[64] = {0};
475 uint16_t m_clientRtpPort = 0;
476 uint16_t m_serverRtpPort = 0;
477 bool m_started =
false;
478 bool m_isPlaying =
false;
479 uint32_t m_lastKeepaliveMs = 0;
480 const uint32_t m_keepaliveIntervalMs = 25000;
483 SingleBuffer<uint8_t> m_pktBuf{0};
484 SingleBuffer<uint8_t> m_tcpCmd{0};
486 char m_bodyBuf[1024];
489 MultiDecoder m_multi_decoder;
490 DecoderNetworkFormat m_decoder_net;
491 DecoderL8 m_decoder_l8;
492 bool m_decoderReady =
false;
493 uint32_t m_idleDelayMs = 10;
494 uint8_t m_payloadOffset = 0;
495 uint8_t m_connectRetries = 2;
496 uint32_t m_connectRetryDelayMs = 500;
497 uint32_t m_headerTimeoutMs = 4000;
500 ResampleStream m_resampler;
501 float m_resampleStep = 1.0f;
505 uint8_t m_payloadType = 0xFF;
506 char m_encoding[32] = {0};
507 AudioInfo m_info{0, 0, 0};
510 m_sessionId[0] =
'\0';
516 m_decoderReady =
false;
517 m_udp_active =
false;
520 void buildUrls(
const char* path) {
521 snprintf(m_baseUrl,
sizeof(m_baseUrl),
"rtsp://%u.%u.%u.%u:%u/", m_addr[0],
522 m_addr[1], m_addr[2], m_addr[3], (
unsigned)m_port);
524 const char* p = path;
526 size_t used = strlen(m_baseUrl);
527 size_t avail =
sizeof(m_baseUrl) - used - 1;
528 if (avail > 0) strncat(m_baseUrl, p, avail);
530 used = strlen(m_baseUrl);
531 if (used > 0 && m_baseUrl[used - 1] !=
'/') {
532 if (used + 1 <
sizeof(m_baseUrl)) {
533 m_baseUrl[used] =
'/';
534 m_baseUrl[used + 1] =
'\0';
538 snprintf(m_trackUrl,
sizeof(m_trackUrl),
"%strackID=0", m_baseUrl);
541 bool openUdpPorts() {
543 for (uint16_t p = 5004; p < 65000; p += 2) {
544 if (m_udp.begin(p)) {
545 LOGI(
"RTSPClient: bound UDP RTP port %u", (
unsigned)p);
554 bool fail(
const char* msg) {
555 LOGE(
"RTSPClient: %s", msg);
560 void maybeKeepalive() {
561 if (!m_started || !m_tcp.connected())
return;
563 if (now - m_lastKeepaliveMs < m_keepaliveIntervalMs)
return;
564 m_lastKeepaliveMs = now;
565 char sessionHdr[128];
566 if (m_sessionId[0]) {
567 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n", m_sessionId);
568 sendSimpleRequest(
"OPTIONS", m_baseUrl, sessionHdr, strlen(sessionHdr),
569 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0,
true);
571 sendSimpleRequest(
"OPTIONS", m_baseUrl,
nullptr, 0, m_hdrBuf,
572 sizeof(m_hdrBuf),
nullptr, 0,
true);
579 size_t computeRtpPayloadOffset(
const uint8_t* data,
size_t length) {
580 if (length <= 12)
return length;
582 uint8_t cc = data[0] & 0x0F;
585 offset += m_payloadOffset;
598 LOGI(
"Still have unprocessed data");
603 int packetSize = m_udp.parsePacket();
604 if (packetSize <= 0) {
605 LOGD(
"packet size: %d", packetSize);
610 if ((
size_t)packetSize > m_pktBuf.size()) m_pktBuf.
resize(packetSize);
611 int n = m_udp.read(m_pktBuf.
data(), packetSize);
614 LOGE(
"packet too small: %d", n);
619 uint8_t* data = m_pktBuf.
data();
620 size_t payloadOffset = computeRtpPayloadOffset(data, (
size_t)n);
621 if (payloadOffset >= (
size_t)n) {
622 LOGW(
"no payload: %d", n);
629 void primeUdpPath() {
630 if (!m_udp_active)
return;
631 if (m_serverRtpPort == 0)
return;
634 for (
int i = 0; i < 2; ++i) {
635 m_udp.beginPacket(m_addr, m_serverRtpPort);
643 bool sniffUdpFor(uint32_t ms) {
644 if (!m_udp_active)
return false;
645 uint32_t start =
millis();
646 while ((
millis() - start) < ms) {
647 int packetSize = m_udp.parsePacket();
648 if (packetSize > 0) {
658 size_t tcpWrite(
const uint8_t* data,
size_t len) {
659 if (m_tcpCmd.size() < 400) m_tcpCmd.
resize(400);
664 bool rc = m_tcp.write(m_tcpCmd.
data(), m_tcpCmd.
available()) ==
670 bool sendSimpleRequest(
const char* method,
const char* url,
671 const char* extraHeaders,
size_t extraLen,
672 char* outHeaders,
size_t outHeadersLen,
char* outBody,
673 size_t outBodyLen,
bool quiet =
false) {
676 int reqLen = snprintf(
677 reqStart,
sizeof(reqStart),
678 "%s %s RTSP/1.0\r\nCSeq: %u\r\nUser-Agent: ArduinoAudioTools\r\n",
679 method, url, (
unsigned)m_cseq++);
680 if (reqLen <= 0)
return false;
683 if (tcpWrite((
const uint8_t*)reqStart, reqLen) != (
size_t)reqLen) {
687 if (extraHeaders && extraLen) {
688 if (tcpWrite((
const uint8_t*)extraHeaders, extraLen) != extraLen) {
693 const char*
end =
"\r\n";
694 if (tcpWrite((
const uint8_t*)
end, 2) != 2) {
699 LOGE(
"TCP write failed");
705 memset(outHeaders, 0, outHeadersLen);
706 if (!readUntilDoubleCRLF(outHeaders, outHeadersLen, hdrUsed,
707 m_headerTimeoutMs)) {
708 if (!quiet) LOGE(
"RTSPClient: header read timeout");
713 int contentLen = parseContentLength(outHeaders);
714 if (outBody && outBodyLen && contentLen > 0) {
715 int toRead = contentLen;
716 if (toRead >= (
int)outBodyLen) toRead = (int)outBodyLen - 1;
717 int got = readExact((uint8_t*)outBody, toRead, 2000);
718 if (got < 0)
return false;
724 bool readUntilDoubleCRLF(
char* buf,
size_t buflen,
int& used,
725 uint32_t timeoutMs = 3000) {
726 uint32_t start =
millis();
729 while ((
millis() - start) < timeoutMs && used < (
int)buflen - 1) {
730 int avail = m_tcp.available();
735 int n = m_tcp.read((uint8_t*)buf + used, 1);
737 char c = buf[used++];
740 state = (c ==
'\r') ? 1 : 0;
743 state = (c ==
'\n') ? 2 : 0;
746 state = (c ==
'\r') ? 3 : 0;
749 state = (c ==
'\n') ? 4 : 0;
762 int readExact(uint8_t* out,
int len, uint32_t timeoutMs) {
763 uint32_t start =
millis();
765 while (got < len && (
millis() - start) < timeoutMs) {
766 int a = m_tcp.available();
771 int n = m_tcp.read(out + got, len - got);
774 return (got == len) ? got : got;
777 static int parseContentLength(
const char* headers) {
778 const char* p = strcasestr(headers,
"Content-Length:");
781 if (sscanf(p,
"Content-Length: %d", &len) == 1)
return len;
785 void parseSessionFromHeaders(
const char* headers) {
786 const char* p = strcasestr(headers,
"Session:");
789 while (*p ==
' ' || *p ==
'\t') ++p;
791 while (*p && *p !=
'\r' && *p !=
'\n' && *p !=
';' &&
792 i <
sizeof(m_sessionId) - 1) {
793 m_sessionId[i++] = *p++;
795 m_sessionId[i] =
'\0';
798 void parseServerPortsFromHeaders(
const char* headers) {
799 const char* t = strcasestr(headers,
"Transport:");
801 const char* s = strcasestr(t,
"server_port=");
803 s += strlen(
"server_port=");
805 if (sscanf(s,
"%d-%d", &a, &b) == 2) {
806 m_serverRtpPort = (uint16_t)a;
811 void parseSdp(
const char* sdp) {
814 while ((p = strcasestr(p,
"a=rtpmap:")) !=
nullptr) {
817 if (sscanf(p,
"%d", &pt) != 1)
continue;
818 const char* space = strchr(p,
' ');
819 if (!space)
continue;
823 while (space[i] && space[i] !=
'/' && space[i] !=
'\r' &&
824 space[i] !=
'\n' && i <
sizeof(m_encoding) - 1) {
825 m_encoding[i] = space[i];
828 m_encoding[i] =
'\0';
829 int rate = 0, ch = 0;
830 const char* afterEnc = space + i;
831 if (*afterEnc ==
'/') {
833 if (sscanf(afterEnc,
"%d/%d", &rate, &ch) < 1) {
838 m_payloadType = (uint8_t)pt;
840 if (strcasecmp(m_encoding,
"L16") == 0) {
841 m_info = AudioInfo(rate, (ch > 0 ? ch : (ch == 0 ? 1 : ch)), 16);
842 }
else if (strcasecmp(m_encoding,
"L8") == 0) {
843 m_info = AudioInfo(rate, (ch > 0 ? ch : (ch == 0 ? 1 : ch)), 8);
845 m_info = AudioInfo();
847 m_multi_decoder.setAudioInfo(m_info);
854 void parseContentBaseFromHeaders(
const char* headers) {
855 m_contentBase[0] =
'\0';
856 if (!headers)
return;
857 const char* p = strcasestr(headers,
"Content-Base:");
859 p += strlen(
"Content-Base:");
860 while (*p ==
' ' || *p ==
'\t') ++p;
862 while (*p && *p !=
'\r' && *p !=
'\n' && i <
sizeof(m_contentBase) - 1) {
863 m_contentBase[i++] = *p++;
865 m_contentBase[i] =
'\0';
867 if (i > 0 && m_contentBase[i - 1] !=
'/') {
868 if (i + 1 <
sizeof(m_contentBase)) {
869 m_contentBase[i++] =
'/';
870 m_contentBase[i] =
'\0';
876 void parseControlFromSdp(
const char* sdp) {
877 m_sdpControl[0] =
'\0';
879 const char* audio = strcasestr(sdp,
"\nm=audio ");
880 const char* searchStart = sdp;
881 const char* searchEnd =
nullptr;
885 const char* nextm = strcasestr(audio + 1,
"\nm=");
886 searchEnd = nextm ? nextm : (sdp + strlen(sdp));
890 searchEnd = sdp + strlen(sdp);
892 const char* p = searchStart;
893 while (p && p < searchEnd) {
894 const char* ctrl = strcasestr(p,
"a=control:");
895 if (!ctrl || ctrl >= searchEnd)
break;
896 ctrl += strlen(
"a=control:");
899 while (ctrl[i] && ctrl[i] !=
'\r' && ctrl[i] !=
'\n' &&
900 i <
sizeof(m_sdpControl) - 1) {
901 m_sdpControl[i] = ctrl[i];
904 m_sdpControl[i] =
'\0';
909 bool isAbsoluteRtspUrl(
const char* url) {
910 if (!url)
return false;
911 return (strncasecmp(url,
"rtsp://", 7) == 0) ||
912 (strncasecmp(url,
"rtsps://", 8) == 0);
915 void buildTrackUrlFromBaseAndControl() {
917 if (m_sdpControl[0] ==
'\0') {
918 snprintf(m_trackUrl,
sizeof(m_trackUrl),
"%strackID=0", m_baseUrl);
921 if (isAbsoluteRtspUrl(m_sdpControl)) {
922 strncpy(m_trackUrl, m_sdpControl,
sizeof(m_trackUrl) - 1);
923 m_trackUrl[
sizeof(m_trackUrl) - 1] =
'\0';
926 const char* base = (m_contentBase[0] ? m_contentBase : m_baseUrl);
927 size_t blen = strlen(base);
931 for (; pos <
sizeof(tmp) - 1 && pos < blen; ++pos) tmp[pos] = base[pos];
932 if (pos > 0 && tmp[pos - 1] !=
'/' && pos <
sizeof(tmp) - 1)
935 const char* ctrl = m_sdpControl;
936 if (*ctrl ==
'/') ++ctrl;
937 while (*ctrl && pos <
sizeof(tmp) - 1) tmp[pos++] = *ctrl++;
939 strncpy(m_trackUrl, tmp,
sizeof(m_trackUrl) - 1);
940 m_trackUrl[
sizeof(m_trackUrl) - 1] =
'\0';