5#include "AudioTools/AudioCodecs/CodecNetworkFormat.h"
6#include "AudioTools/AudioCodecs/MultiDecoder.h"
7#include "AudioTools/CoreAudio//BaseStream.h"
8#include "AudioTools/CoreAudio/AudioBasic/Collections/Vector.h"
9#include "AudioTools/CoreAudio/Buffers.h"
10#include "AudioTools/CoreAudio/ResampleStream.h"
38template <
typename TcpClient,
typename UdpSocket>
43 m_multi_decoder.
addDecoder(m_decoder_net,
"audio/L16");
45 m_multi_decoder.
addDecoder(m_decoder_l8,
"audio/L8");
85 if (factor <= 0.0f) factor = 1.0f;
86 float step = 1.0f / factor;
87 m_resampleStep = step;
129 bool begin(IPAddress addr, uint16_t port,
const char* path =
nullptr) {
134 if (m_tcp.connected()) m_tcp.stop();
135 LOGI(
"RTSPClient: connecting to %u.%u.%u.%u:%u", m_addr[0], m_addr[1],
136 m_addr[2], m_addr[3], (
unsigned)m_port);
138 bool connected =
false;
139 for (uint8_t attempt = 0; attempt <= m_connectRetries; ++attempt) {
140 if (m_tcp.connect(m_addr, m_port)) {
144 LOGW(
"RTSPClient: connect attempt %u failed", (
unsigned)(attempt + 1));
145 if (attempt < m_connectRetries) delay(m_connectRetryDelayMs);
148 LOGE(
"RTSPClient: TCP connect failed");
151 m_tcp.setNoDelay(
true);
161 int retry = m_connectRetries;
162 while (!sendSimpleRequest(
"OPTIONS", m_baseUrl,
nullptr, 0, m_hdrBuf,
163 sizeof(m_hdrBuf),
nullptr, 0)) {
165 return fail(
"OPTIONS failed");
167 LOGW(
"RTSPClient: retrying OPTIONS");
174 const char* describeExtra =
"Accept: application/sdp\r\n";
175 if (!sendSimpleRequest(
"DESCRIBE", m_baseUrl, describeExtra,
176 strlen(describeExtra), m_hdrBuf,
sizeof(m_hdrBuf),
177 m_bodyBuf,
sizeof(m_bodyBuf)))
178 return fail(
"DESCRIBE failed");
183 parseContentBaseFromHeaders(m_hdrBuf);
185 parseControlFromSdp(m_bodyBuf);
186 buildTrackUrlFromBaseAndControl();
187 LOGI(
"RTSPClient: SDP control='%s' content-base='%s'", m_sdpControl,
189 LOGI(
"RTSPClient: SETUP url: %s", m_trackUrl);
192 if (!openUdpPorts())
return fail(
"UDP bind failed");
195 char transportHdr[128];
196 snprintf(transportHdr,
sizeof(transportHdr),
197 "Transport: RTP/AVP;unicast;client_port=%u-%u\r\n",
198 (
unsigned)m_clientRtpPort, (
unsigned)(m_clientRtpPort + 1));
199 if (!sendSimpleRequest(
"SETUP", m_trackUrl, transportHdr,
200 strlen(transportHdr), m_hdrBuf,
sizeof(m_hdrBuf),
203 snprintf(transportHdr,
sizeof(transportHdr),
204 "Transport: RTP/AVP/UDP;unicast;client_port=%u-%u\r\n",
205 (
unsigned)m_clientRtpPort, (
unsigned)(m_clientRtpPort + 1));
206 if (!sendSimpleRequest(
"SETUP", m_trackUrl, transportHdr,
207 strlen(transportHdr), m_hdrBuf,
sizeof(m_hdrBuf),
209 return fail(
"SETUP failed");
214 parseSessionFromHeaders(m_hdrBuf);
215 parseServerPortsFromHeaders(m_hdrBuf);
216 if (m_sessionId[0] ==
'\0')
return fail(
"Missing Session ID");
223 char sessionHdr[128];
224 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n", m_sessionId);
225 if (!sendSimpleRequest(
"PLAY", m_baseUrl, sessionHdr, strlen(sessionHdr),
226 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0)) {
230 if (sniffUdpFor(1500)) {
231 LOGW(
"RTSPClient: proceeding without PLAY response (RTP detected)");
233 return fail(
"PLAY failed");
239 m_lastKeepaliveMs =
millis();
245 operator bool() {
return m_started &&
mime() !=
nullptr &&
available() > 0; }
253 if (m_tcp.connected()) {
254 char sessionHdr[128];
255 if (m_sessionId[0]) {
256 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n",
258 sendSimpleRequest(
"TEARDOWN", m_baseUrl, sessionHdr,
259 strlen(sessionHdr), m_hdrBuf,
sizeof(m_hdrBuf),
268 if (m_tcp.connected()) m_tcp.stop();
278 delay(m_idleDelayMs);
284 delay(m_idleDelayMs);
289 if (avail == 0) delay(m_idleDelayMs);
299 switch (m_payloadType) {
310 return "audio/adpcm";
324 if (strcasecmp(m_encoding,
"L16") == 0)
return "audio/L16";
325 if (strcasecmp(m_encoding,
"L8") == 0)
return "audio/L8";
326 if (strcasecmp(m_encoding,
"PCMU") == 0)
return "audio/PCMU";
327 if (strcasecmp(m_encoding,
"PCMA") == 0)
return "audio/PCMA";
328 if (strcasecmp(m_encoding,
"GSM") == 0)
return "audio/gsm";
329 if (strcasecmp(m_encoding,
"MPA") == 0)
return "audio/mpeg";
330 if (strcasecmp(m_encoding,
"MPEG4-GENERIC") == 0)
return "audio/aac";
331 if (strcasecmp(m_encoding,
"OPUS") == 0)
return "audio/opus";
332 if (strcasecmp(m_encoding,
"DVI4") == 0)
return "audio/adpcm";
347 if (!m_started || !m_tcp.connected() || m_sessionId[0] ==
'\0')
349 if (active == m_isPlaying)
return true;
351 char sessionHdr[128];
352 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n", m_sessionId);
355 ok = sendSimpleRequest(
"PLAY", m_baseUrl, sessionHdr, strlen(sessionHdr),
356 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0);
357 if (ok) m_isPlaying =
true;
359 ok = sendSimpleRequest(
"PAUSE", m_baseUrl, sessionHdr, strlen(sessionHdr),
360 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0);
376 m_multi_decoder.
addDecoder(decoder, mimeType);
386 delay(m_idleDelayMs);
394 delay(m_idleDelayMs);
401 if (m_pktBuf.isEmpty()) {
403 delay(m_idleDelayMs);
408 if (!m_decoderReady) {
409 const char* m =
mime();
411 LOGI(
"Selecting decoder: %s", m);
415 if (m_multi_decoder.getOutput() !=
nullptr) {
416 m_multi_decoder.
begin();
418 m_decoderReady =
true;
423 size_t written = m_multi_decoder.
write(m_pktBuf.
data(), n);
424 LOGI(
"copy: %d -> %d", (
int)n, (
int)written);
449 void setNotifyActive(
bool flag) { m_multi_decoder.
setNotifyActive(flag); }
450 bool isNotifyActive() {
return m_multi_decoder.
isNotifyActive(); }
456 bool m_udp_active =
false;
462 char m_baseUrl[96] = {0};
463 char m_trackUrl[128] = {0};
464 char m_contentBase[160] = {0};
465 char m_sdpControl[128] = {0};
466 char m_sessionId[64] = {0};
467 uint16_t m_clientRtpPort = 0;
468 uint16_t m_serverRtpPort = 0;
469 bool m_started =
false;
470 bool m_isPlaying =
false;
471 uint32_t m_lastKeepaliveMs = 0;
472 const uint32_t m_keepaliveIntervalMs = 25000;
475 SingleBuffer<uint8_t> m_pktBuf{0};
476 SingleBuffer<uint8_t> m_tcpCmd{0};
478 char m_bodyBuf[1024];
481 MultiDecoder m_multi_decoder;
482 DecoderNetworkFormat m_decoder_net;
483 DecoderL8 m_decoder_l8;
484 bool m_decoderReady =
false;
485 uint32_t m_idleDelayMs = 10;
486 uint8_t m_payloadOffset = 0;
487 uint8_t m_connectRetries = 2;
488 uint32_t m_connectRetryDelayMs = 500;
489 uint32_t m_headerTimeoutMs = 4000;
492 ResampleStream m_resampler;
493 float m_resampleStep = 1.0f;
497 uint8_t m_payloadType = 0xFF;
498 char m_encoding[32] = {0};
499 AudioInfo m_info{0, 0, 0};
502 m_sessionId[0] =
'\0';
508 m_decoderReady =
false;
509 m_udp_active =
false;
512 void buildUrls(
const char* path) {
513 snprintf(m_baseUrl,
sizeof(m_baseUrl),
"rtsp://%u.%u.%u.%u:%u/", m_addr[0],
514 m_addr[1], m_addr[2], m_addr[3], (
unsigned)m_port);
516 const char* p = path;
518 size_t used = strlen(m_baseUrl);
519 size_t avail =
sizeof(m_baseUrl) - used - 1;
520 if (avail > 0) strncat(m_baseUrl, p, avail);
522 used = strlen(m_baseUrl);
523 if (used > 0 && m_baseUrl[used - 1] !=
'/') {
524 if (used + 1 <
sizeof(m_baseUrl)) {
525 m_baseUrl[used] =
'/';
526 m_baseUrl[used + 1] =
'\0';
530 snprintf(m_trackUrl,
sizeof(m_trackUrl),
"%strackID=0", m_baseUrl);
533 bool openUdpPorts() {
535 for (uint16_t p = 5004; p < 65000; p += 2) {
536 if (m_udp.begin(p)) {
537 LOGI(
"RTSPClient: bound UDP RTP port %u", (
unsigned)p);
546 bool fail(
const char* msg) {
547 LOGE(
"RTSPClient: %s", msg);
552 void maybeKeepalive() {
553 if (!m_started || !m_tcp.connected())
return;
555 if (now - m_lastKeepaliveMs < m_keepaliveIntervalMs)
return;
556 m_lastKeepaliveMs = now;
557 char sessionHdr[128];
558 if (m_sessionId[0]) {
559 snprintf(sessionHdr,
sizeof(sessionHdr),
"Session: %s\r\n", m_sessionId);
560 sendSimpleRequest(
"OPTIONS", m_baseUrl, sessionHdr, strlen(sessionHdr),
561 m_hdrBuf,
sizeof(m_hdrBuf),
nullptr, 0,
true);
563 sendSimpleRequest(
"OPTIONS", m_baseUrl,
nullptr, 0, m_hdrBuf,
564 sizeof(m_hdrBuf),
nullptr, 0,
true);
571 size_t computeRtpPayloadOffset(
const uint8_t* data,
size_t length) {
572 if (length <= 12)
return length;
574 uint8_t cc = data[0] & 0x0F;
577 offset += m_payloadOffset;
590 LOGI(
"Still have unprocessed data");
595 int packetSize = m_udp.parsePacket();
596 if (packetSize <= 0) {
597 LOGD(
"packet size: %d", packetSize);
602 if ((
size_t)packetSize > m_pktBuf.size()) m_pktBuf.
resize(packetSize);
603 int n = m_udp.read(m_pktBuf.
data(), packetSize);
606 LOGE(
"packet too small: %d", n);
611 uint8_t* data = m_pktBuf.
data();
612 size_t payloadOffset = computeRtpPayloadOffset(data, (
size_t)n);
613 if (payloadOffset >= (
size_t)n) {
614 LOGW(
"no payload: %d", n);
621 void primeUdpPath() {
622 if (!m_udp_active)
return;
623 if (m_serverRtpPort == 0)
return;
626 for (
int i = 0; i < 2; ++i) {
627 m_udp.beginPacket(m_addr, m_serverRtpPort);
635 bool sniffUdpFor(uint32_t ms) {
636 if (!m_udp_active)
return false;
637 uint32_t start =
millis();
638 while ((
millis() - start) < ms) {
639 int packetSize = m_udp.parsePacket();
640 if (packetSize > 0) {
650 size_t tcpWrite(
const uint8_t* data,
size_t len) {
651 if (m_tcpCmd.size() < 400) m_tcpCmd.
resize(400);
656 bool rc = m_tcp.write(m_tcpCmd.
data(), m_tcpCmd.
available()) ==
662 bool sendSimpleRequest(
const char* method,
const char* url,
663 const char* extraHeaders,
size_t extraLen,
664 char* outHeaders,
size_t outHeadersLen,
char* outBody,
665 size_t outBodyLen,
bool quiet =
false) {
668 int reqLen = snprintf(
669 reqStart,
sizeof(reqStart),
670 "%s %s RTSP/1.0\r\nCSeq: %u\r\nUser-Agent: ArduinoAudioTools\r\n",
671 method, url, (
unsigned)m_cseq++);
672 if (reqLen <= 0)
return false;
675 if (tcpWrite((
const uint8_t*)reqStart, reqLen) != (
size_t)reqLen) {
679 if (extraHeaders && extraLen) {
680 if (tcpWrite((
const uint8_t*)extraHeaders, extraLen) != extraLen) {
685 const char*
end =
"\r\n";
686 if (tcpWrite((
const uint8_t*)
end, 2) != 2) {
691 LOGE(
"TCP write failed");
697 memset(outHeaders, 0, outHeadersLen);
698 if (!readUntilDoubleCRLF(outHeaders, outHeadersLen, hdrUsed,
699 m_headerTimeoutMs)) {
700 if (!quiet) LOGE(
"RTSPClient: header read timeout");
705 int contentLen = parseContentLength(outHeaders);
706 if (outBody && outBodyLen && contentLen > 0) {
707 int toRead = contentLen;
708 if (toRead >= (
int)outBodyLen) toRead = (int)outBodyLen - 1;
709 int got = readExact((uint8_t*)outBody, toRead, 2000);
710 if (got < 0)
return false;
716 bool readUntilDoubleCRLF(
char* buf,
size_t buflen,
int& used,
717 uint32_t timeoutMs = 3000) {
718 uint32_t start =
millis();
721 while ((
millis() - start) < timeoutMs && used < (
int)buflen - 1) {
722 int avail = m_tcp.available();
727 int n = m_tcp.read((uint8_t*)buf + used, 1);
729 char c = buf[used++];
732 state = (c ==
'\r') ? 1 : 0;
735 state = (c ==
'\n') ? 2 : 0;
738 state = (c ==
'\r') ? 3 : 0;
741 state = (c ==
'\n') ? 4 : 0;
754 int readExact(uint8_t* out,
int len, uint32_t timeoutMs) {
755 uint32_t start =
millis();
757 while (got < len && (
millis() - start) < timeoutMs) {
758 int a = m_tcp.available();
763 int n = m_tcp.read(out + got, len - got);
766 return (got == len) ? got : got;
769 static int parseContentLength(
const char* headers) {
770 const char* p = strcasestr(headers,
"Content-Length:");
773 if (sscanf(p,
"Content-Length: %d", &len) == 1)
return len;
777 void parseSessionFromHeaders(
const char* headers) {
778 const char* p = strcasestr(headers,
"Session:");
781 while (*p ==
' ' || *p ==
'\t') ++p;
783 while (*p && *p !=
'\r' && *p !=
'\n' && *p !=
';' &&
784 i <
sizeof(m_sessionId) - 1) {
785 m_sessionId[i++] = *p++;
787 m_sessionId[i] =
'\0';
790 void parseServerPortsFromHeaders(
const char* headers) {
791 const char* t = strcasestr(headers,
"Transport:");
793 const char* s = strcasestr(t,
"server_port=");
795 s += strlen(
"server_port=");
797 if (sscanf(s,
"%d-%d", &a, &b) == 2) {
798 m_serverRtpPort = (uint16_t)a;
803 void parseSdp(
const char* sdp) {
806 while ((p = strcasestr(p,
"a=rtpmap:")) !=
nullptr) {
809 if (sscanf(p,
"%d", &pt) != 1)
continue;
810 const char* space = strchr(p,
' ');
811 if (!space)
continue;
815 while (space[i] && space[i] !=
'/' && space[i] !=
'\r' &&
816 space[i] !=
'\n' && i <
sizeof(m_encoding) - 1) {
817 m_encoding[i] = space[i];
820 m_encoding[i] =
'\0';
821 int rate = 0, ch = 0;
822 const char* afterEnc = space + i;
823 if (*afterEnc ==
'/') {
825 if (sscanf(afterEnc,
"%d/%d", &rate, &ch) < 1) {
830 m_payloadType = (uint8_t)pt;
832 if (strcasecmp(m_encoding,
"L16") == 0) {
833 m_info = AudioInfo(rate, (ch > 0 ? ch : (ch == 0 ? 1 : ch)), 16);
834 }
else if (strcasecmp(m_encoding,
"L8") == 0) {
835 m_info = AudioInfo(rate, (ch > 0 ? ch : (ch == 0 ? 1 : ch)), 8);
837 m_info = AudioInfo();
839 m_multi_decoder.setAudioInfo(m_info);
846 void parseContentBaseFromHeaders(
const char* headers) {
847 m_contentBase[0] =
'\0';
848 if (!headers)
return;
849 const char* p = strcasestr(headers,
"Content-Base:");
851 p += strlen(
"Content-Base:");
852 while (*p ==
' ' || *p ==
'\t') ++p;
854 while (*p && *p !=
'\r' && *p !=
'\n' && i <
sizeof(m_contentBase) - 1) {
855 m_contentBase[i++] = *p++;
857 m_contentBase[i] =
'\0';
859 if (i > 0 && m_contentBase[i - 1] !=
'/') {
860 if (i + 1 <
sizeof(m_contentBase)) {
861 m_contentBase[i++] =
'/';
862 m_contentBase[i] =
'\0';
868 void parseControlFromSdp(
const char* sdp) {
869 m_sdpControl[0] =
'\0';
871 const char* audio = strcasestr(sdp,
"\nm=audio ");
872 const char* searchStart = sdp;
873 const char* searchEnd =
nullptr;
877 const char* nextm = strcasestr(audio + 1,
"\nm=");
878 searchEnd = nextm ? nextm : (sdp + strlen(sdp));
882 searchEnd = sdp + strlen(sdp);
884 const char* p = searchStart;
885 while (p && p < searchEnd) {
886 const char* ctrl = strcasestr(p,
"a=control:");
887 if (!ctrl || ctrl >= searchEnd)
break;
888 ctrl += strlen(
"a=control:");
891 while (ctrl[i] && ctrl[i] !=
'\r' && ctrl[i] !=
'\n' &&
892 i <
sizeof(m_sdpControl) - 1) {
893 m_sdpControl[i] = ctrl[i];
896 m_sdpControl[i] =
'\0';
901 bool isAbsoluteRtspUrl(
const char* url) {
902 if (!url)
return false;
903 return (strncasecmp(url,
"rtsp://", 7) == 0) ||
904 (strncasecmp(url,
"rtsps://", 8) == 0);
907 void buildTrackUrlFromBaseAndControl() {
909 if (m_sdpControl[0] ==
'\0') {
910 snprintf(m_trackUrl,
sizeof(m_trackUrl),
"%strackID=0", m_baseUrl);
913 if (isAbsoluteRtspUrl(m_sdpControl)) {
914 strncpy(m_trackUrl, m_sdpControl,
sizeof(m_trackUrl) - 1);
915 m_trackUrl[
sizeof(m_trackUrl) - 1] =
'\0';
918 const char* base = (m_contentBase[0] ? m_contentBase : m_baseUrl);
919 size_t blen = strlen(base);
923 for (; pos <
sizeof(tmp) - 1 && pos < blen; ++pos) tmp[pos] = base[pos];
924 if (pos > 0 && tmp[pos - 1] !=
'/' && pos <
sizeof(tmp) - 1)
927 const char* ctrl = m_sdpControl;
928 if (*ctrl ==
'/') ++ctrl;
929 while (*ctrl && pos <
sizeof(tmp) - 1) tmp[pos++] = *ctrl++;
931 strncpy(m_trackUrl, tmp,
sizeof(m_trackUrl) - 1);
932 m_trackUrl[
sizeof(m_trackUrl) - 1] =
'\0';