arduino-audio-tools
Loading...
Searching...
No Matches
RTSPClient.h
Go to the documentation of this file.
1/*
2 * Author: Phil Schatzmann
3 *
4 * Based on Micro-RTSP library:
5 * https://github.com/geeksville/Micro-RTSP
6 * https://github.com/Tomp0801/Micro-RTSP-Audio
7 *
8 */
9#pragma once
10#include <Arduino.h>
11#include <IPAddress.h>
12
19
20namespace audio_tools {
21
46template <typename TcpClient, typename UdpSocket>
48 public:
50 // convert network format to little endian
52 // convert to 16 bit
54 // Start resampler; it will receive AudioInfo later via setAudioInfo
57 }
58
82 void setOutput(Print& out) { m_resampler.setOutput(out); }
83
92 void setResampleFactor(float factor) {
93 if (factor <= 0.0f) factor = 1.0f;
94 float step = 1.0f / factor;
95 m_resampleStep = step;
97 // Always route via resampler; factor 1.0 is pass-through
98 }
103 void setIdleDelay(uint32_t ms) { m_idleDelayMs = ms; }
104
108 void setConnectRetries(uint8_t retries) { m_connectRetries = retries; }
109
114
119 void setHeaderTimeoutMs(uint32_t ms) { m_headerTimeoutMs = ms; }
120
127 void setPayloadOffset(uint8_t bytes) { m_payloadOffset = bytes; }
137 bool begin(IPAddress addr, uint16_t port, const char* path = nullptr) {
138 resetState();
139 m_addr = addr;
140 m_port = port;
141
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);
145 // m_tcp.setTimeout(m_headerTimeoutMs / 1000);
146 bool connected = false;
147 for (uint8_t attempt = 0; attempt <= m_connectRetries; ++attempt) {
148 if (m_tcp.connect(m_addr, m_port)) {
149 connected = true;
150 break;
151 }
152 LOGW("RTSPClient: connect attempt %u failed", (unsigned)(attempt + 1));
154 }
155 if (!connected) {
156 LOGE("RTSPClient: TCP connect failed");
157 return false;
158 }
159 m_tcp.setNoDelay(true);
160
161 // Build base URL and track URL
162 buildUrls(path);
163
164 // CSeq starts at 1
165 m_cseq = 1;
166
167 // OPTIONS
168 LOGI("OPTIONS");
169 int retry = m_connectRetries;
170 while (!sendSimpleRequest("OPTIONS", m_baseUrl, nullptr, 0, m_hdrBuf,
171 sizeof(m_hdrBuf), nullptr, 0)) {
172 if (--retry == 0) {
173 return fail("OPTIONS failed");
174 } else {
175 LOGW("RTSPClient: retrying OPTIONS");
176 delay(800);
177 }
178 }
179
180 // DESCRIBE
181 LOGI("DESCRIBE");
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");
187
188 // Parse SDP (rtpmap) to capture payload and encoding
190 // Parse Content-Base for absolute/relative control resolution
192 // Parse a=control and build the correct track URL for SETUP
195 LOGI("RTSPClient: SDP control='%s' content-base='%s'", m_sdpControl,
197 LOGI("RTSPClient: SETUP url: %s", m_trackUrl);
198
199 // Prepare UDP (client_port)
200 if (!openUdpPorts()) return fail("UDP bind failed");
201
202 // SETUP with client_port pair
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),
209 nullptr, 0)) {
210 // Fallback: some servers require explicit UDP in transport profile
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),
216 nullptr, 0)) {
217 return fail("SETUP failed");
218 }
219 }
220
221 // Parse Session and server_port from last headers
224 if (m_sessionId[0] == '\0') return fail("Missing Session ID");
225
226 // Prime UDP path to server RTP port (helps some networks/servers)
227 primeUdpPath();
228
229 // PLAY
230 LOGI("PLAY");
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)) {
235 // Some servers start streaming RTP immediately but delay/omit PLAY
236 // response Treat PLAY as successful if RTP arrives shortly after sending
237 // PLAY
238 if (sniffUdpFor(1500)) {
239 LOGW("RTSPClient: proceeding without PLAY response (RTP detected)");
240 } else {
241 return fail("PLAY failed");
242 }
243 }
244
245 m_started = true;
246 m_isPlaying = true;
248 return true;
249 }
250
253 operator bool() { return m_started && mime() != nullptr && available() > 0; }
254
258 void end() {
259 if (m_started) {
260 // best-effort TEARDOWN
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),
268 nullptr, 0, /*quiet*/ true);
269 }
270 }
271 }
272
273 if (m_udp_active) {
274 m_udp.stop();
275 }
276 if (m_tcp.connected()) m_tcp.stop();
277 m_started = false;
278 m_isPlaying = false;
279 }
280
284 int available() {
285 if (!m_started) {
287 return 0;
288 }
289 // keepalive regardless of play state
291 if (!m_isPlaying) {
293 return 0;
294 }
295 serviceUdp();
296 int avail = m_pktBuf.available();
297 if (avail == 0) delay(m_idleDelayMs);
298 return avail;
299 }
300
305 const char* mime() const {
306 // Prefer static RTP payload type mapping when available
307 switch (m_payloadType) {
308 case 0: // PCMU
309 return "audio/PCMU";
310 case 3: // GSM
311 return "audio/gsm";
312 case 4: // G723
313 return "audio/g723";
314 case 5: // DVI4/8000 (IMA ADPCM)
315 case 6: // DVI4/16000
316 case 16: // DVI4/11025
317 case 17: // DVI4/22050
318 return "audio/adpcm";
319 case 8: // PCMA
320 return "audio/PCMA";
321 case 9: // G722
322 return "audio/g722";
323 case 10: // L16 stereo
324 case 11: // L16 mono
325 return "audio/L16";
326 case 14: // MPA (MPEG audio / MP3)
327 return "audio/mpeg";
328 default:
329 break; // dynamic or unknown; fall back to SDP encoding string
330 }
331 // Fallback: infer from SDP encoding token
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"; // MP3
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"; // IMA ADPCM
341 return nullptr;
342 }
343
347 uint8_t payloadType() const { return m_payloadType; }
348
354 bool setActive(bool active) {
355 if (!m_started || !m_tcp.connected() || m_sessionId[0] == '\0')
356 return false;
357 if (active == m_isPlaying) return true; // no-op
358
359 char sessionHdr[128];
360 snprintf(sessionHdr, sizeof(sessionHdr), "Session: %s\r\n", m_sessionId);
361 bool ok;
362 if (active) {
363 ok = sendSimpleRequest("PLAY", m_baseUrl, sessionHdr, strlen(sessionHdr),
364 m_hdrBuf, sizeof(m_hdrBuf), nullptr, 0);
365 if (ok) m_isPlaying = true;
366 } else {
367 ok = sendSimpleRequest("PAUSE", m_baseUrl, sessionHdr, strlen(sessionHdr),
368 m_hdrBuf, sizeof(m_hdrBuf), nullptr, 0);
369 if (ok) {
370 m_isPlaying = false;
371 // drop any buffered payload
372 m_pktBuf.clear();
373 }
374 }
375 return ok;
376 }
377
383 void addDecoder(const char* mimeType, AudioDecoder& decoder) {
384 m_multi_decoder.addDecoder(decoder, mimeType);
385 }
386
391 void addDecoder(AudioDecoder& decoder) {
392 addDecoder(decoder.mime(), decoder);
393 }
394
400 size_t copy() {
401 if (!m_started) {
403 LOGD("not started");
404 return 0;
405 }
406
408
409 if (!m_isPlaying) {
411 LOGD("not playing");
412 return 0;
413 }
414
415 serviceUdp();
416
417 if (m_pktBuf.isEmpty()) {
418 LOGD("no data");
420 return 0;
421 }
422
423 // On first data, make sure decoder selection and audio info are applied
424 if (!m_decoderReady) {
425 const char* m = mime();
426 if (m) {
427 LOGI("Selecting decoder: %s", m);
428 // Ensure network format decoder has correct PCM info
431 if (m_multi_decoder.getOutput() != nullptr) {
432 m_multi_decoder.begin(); // start decoder only when output is defined
433 }
434 m_decoderReady = true;
435 }
436 }
437
438 int n = m_pktBuf.available();
439 size_t written = m_multi_decoder.write(m_pktBuf.data(), n);
440 LOGI("copy: %d -> %d", (int)n, (int)written);
441 m_pktBuf.clearArray(written);
442 return written;
443 }
444
450
451 void setAudioInfo(AudioInfo info) override {
453 }
454
455 // AudioInfoSource forwarding: delegate notifications to MultiDecoder
467
468 protected:
469 // Connection
470 TcpClient m_tcp;
471 UdpSocket m_udp;
472 bool m_udp_active = false;
474 uint16_t m_port = 0;
475
476 // RTSP state
477 uint32_t m_cseq = 1;
478 char m_baseUrl[96] = {0};
479 char m_trackUrl[128] = {0};
480 char m_contentBase[160] = {0};
481 char m_sdpControl[128] = {0};
482 char m_sessionId[64] = {0};
483 uint16_t m_clientRtpPort = 0; // even
484 uint16_t m_serverRtpPort = 0; // optional from Transport response
485 bool m_started = false;
486 bool m_isPlaying = false;
487 uint32_t m_lastKeepaliveMs = 0;
488 const uint32_t m_keepaliveIntervalMs = 25000; // 25s
489
490 // Buffers
493 char m_hdrBuf[1024];
494 char m_bodyBuf[1024];
495
496 // Decoder pipeline
500 bool m_decoderReady = false;
501 uint32_t m_idleDelayMs = 10;
502 uint8_t m_payloadOffset = 0; // extra bytes after RTP header/CSRCs
503 uint8_t m_connectRetries = 2;
504 uint32_t m_connectRetryDelayMs = 500;
505 uint32_t m_headerTimeoutMs = 4000; // header read timeout
506
507 // Resampling pipeline
509 float m_resampleStep = 1.0f;
510 // Sinks are set directly on the resampler
511
512 // --- RTP/SDP fields ---
513 uint8_t m_payloadType = 0xFF; // unknown by default
514 char m_encoding[32] = {0};
516
517 void resetState() {
518 m_sessionId[0] = '\0';
519 m_serverRtpPort = 0;
520 m_clientRtpPort = 0;
521 m_cseq = 1;
522 m_pktBuf.resize(2048);
523 m_pktBuf.clear();
524 m_decoderReady = false;
525 m_udp_active = false;
526 }
527
528 void buildUrls(const char* path) {
529 snprintf(m_baseUrl, sizeof(m_baseUrl), "rtsp://%u.%u.%u.%u:%u/", m_addr[0],
530 m_addr[1], m_addr[2], m_addr[3], (unsigned)m_port);
531 if (path && *path) {
532 const char* p = path;
533 if (*p == '/') ++p; // skip leading slash
534 size_t used = strlen(m_baseUrl);
535 size_t avail = sizeof(m_baseUrl) - used - 1;
536 if (avail > 0) strncat(m_baseUrl, p, avail);
537 // ensure trailing '/'
538 used = strlen(m_baseUrl);
539 if (used > 0 && m_baseUrl[used - 1] != '/') {
540 if (used + 1 < sizeof(m_baseUrl)) {
541 m_baseUrl[used] = '/';
542 m_baseUrl[used + 1] = '\0';
543 }
544 }
545 }
546 snprintf(m_trackUrl, sizeof(m_trackUrl), "%strackID=0", m_baseUrl);
547 }
548
550 // Try a few even RTP ports starting at 5004
551 for (uint16_t p = 5004; p < 65000; p += 2) {
552 if (m_udp.begin(p)) {
553 LOGI("RTSPClient: bound UDP RTP port %u", (unsigned)p);
554 m_clientRtpPort = p;
555 m_udp_active = true;
556 return true;
557 }
558 }
559 return false;
560 }
561
562 bool fail(const char* msg) {
563 LOGE("RTSPClient: %s", msg);
564 end();
565 return false;
566 }
567
569 if (!m_started || !m_tcp.connected()) return;
570 uint32_t now = millis();
571 if (now - m_lastKeepaliveMs < m_keepaliveIntervalMs) return;
572 m_lastKeepaliveMs = now;
573 char sessionHdr[128];
574 if (m_sessionId[0]) {
575 snprintf(sessionHdr, sizeof(sessionHdr), "Session: %s\r\n", m_sessionId);
576 sendSimpleRequest("OPTIONS", m_baseUrl, sessionHdr, strlen(sessionHdr),
577 m_hdrBuf, sizeof(m_hdrBuf), nullptr, 0, /*quiet*/ true);
578 } else {
579 sendSimpleRequest("OPTIONS", m_baseUrl, nullptr, 0, m_hdrBuf,
580 sizeof(m_hdrBuf), nullptr, 0, /*quiet*/ true);
581 }
582 }
583
584 // Compute the RTP payload offset inside a UDP packet
585 // Considers fixed RTP header (12 bytes), CSRC count, and configured extra
586 // offset
587 size_t computeRtpPayloadOffset(const uint8_t* data, size_t length) {
588 if (length <= 12) return length;
589 size_t offset = 12;
590 uint8_t cc = data[0] & 0x0F; // CSRC count
591 offset += cc * 4;
592 // Apply any configured additional payload offset (e.g., RFC2250)
593 offset += m_payloadOffset;
594 return offset;
595 }
596
597 void serviceUdp() {
598 // Keep RTSP session alive
600
601 if (!m_udp_active) {
602 LOGE("no UDP");
603 return;
604 }
605 if (m_pktBuf.available() > 0) {
606 LOGI("Still have unprocessed data");
607 return; // still have data buffered
608 }
609
610 // parse next UDP packet
611 int packetSize = m_udp.parsePacket();
612 if (packetSize <= 0) {
613 LOGD("packet size: %d", packetSize);
614 return;
615 }
616
617 // Fill buffer
618 if ((size_t)packetSize > m_pktBuf.size()) m_pktBuf.resize(packetSize);
619 int n = m_udp.read(m_pktBuf.data(), packetSize);
621 if (n <= 12) {
622 LOGE("packet too small: %d", n);
623 return; // too small to contain RTP
624 }
625
626 // Very basic RTP parsing: compute payload offset
627 uint8_t* data = m_pktBuf.data();
628 size_t payloadOffset = computeRtpPayloadOffset(data, (size_t)n);
629 if (payloadOffset >= (size_t)n) {
630 LOGW("no payload: %d", n);
631 }
632
633 // move payload to beginning for contiguous read
634 m_pktBuf.clearArray(payloadOffset);
635 }
636
638 if (!m_udp_active) return;
639 if (m_serverRtpPort == 0) return;
640 // Send a tiny datagram to server RTP port to open NAT/flows
641 // Not required by RTSP, but improves interoperability
642 for (int i = 0; i < 2; ++i) {
643 m_udp.beginPacket(m_addr, m_serverRtpPort);
644 uint8_t b = 0x00;
645 m_udp.write(&b, 1);
646 m_udp.endPacket();
647 delay(2);
648 }
649 }
650
651 bool sniffUdpFor(uint32_t ms) {
652 if (!m_udp_active) return false;
653 uint32_t start = millis();
654 while ((millis() - start) < ms) {
655 int packetSize = m_udp.parsePacket();
656 if (packetSize > 0) {
657 // restore to be processed by normal path
658 return true;
659 }
660 delay(5);
661 }
662 return false;
663 }
664
665 // Centralized TCP write helper
666 size_t tcpWrite(const uint8_t* data, size_t len) {
667 if (m_tcpCmd.size() < 400) m_tcpCmd.resize(400);
668 return m_tcpCmd.writeArray(data, len);
669 }
670
671 bool tcpCommit() {
672 bool rc = m_tcp.write(m_tcpCmd.data(), m_tcpCmd.available()) ==
674 m_tcpCmd.clear();
675 return rc;
676 }
677
678 bool sendSimpleRequest(const char* method, const char* url,
679 const char* extraHeaders, size_t extraLen,
680 char* outHeaders, size_t outHeadersLen, char* outBody,
681 size_t outBodyLen, bool quiet = false) {
682 // Build request
683 char reqStart[256];
684 int reqLen = snprintf(
685 reqStart, sizeof(reqStart),
686 "%s %s RTSP/1.0\r\nCSeq: %u\r\nUser-Agent: ArduinoAudioTools\r\n",
687 method, url, (unsigned)m_cseq++);
688 if (reqLen <= 0) return false;
689
690 // Send start line + mandatory headers
691 if (tcpWrite((const uint8_t*)reqStart, reqLen) != (size_t)reqLen) {
692 return false;
693 }
694 // Optional extra headers
695 if (extraHeaders && extraLen) {
696 if (tcpWrite((const uint8_t*)extraHeaders, extraLen) != extraLen) {
697 return false;
698 }
699 }
700 // End of headers
701 const char* end = "\r\n";
702 if (tcpWrite((const uint8_t*)end, 2) != 2) {
703 return false;
704 }
705
706 if (!tcpCommit()) {
707 LOGE("TCP write failed");
708 return false;
709 }
710
711 // Read response headers until CRLFCRLF
712 int hdrUsed = 0;
713 memset(outHeaders, 0, outHeadersLen);
714 if (!readUntilDoubleCRLF(outHeaders, outHeadersLen, hdrUsed,
716 if (!quiet) LOGE("RTSPClient: header read timeout");
717 return false;
718 }
719
720 // Optionally read body based on Content-Length
721 int contentLen = parseContentLength(outHeaders);
722 if (outBody && outBodyLen && contentLen > 0) {
723 int toRead = contentLen;
724 if (toRead >= (int)outBodyLen) toRead = (int)outBodyLen - 1;
725 int got = readExact((uint8_t*)outBody, toRead, 2000);
726 if (got < 0) return false;
727 outBody[got] = '\0';
728 }
729 return true;
730 }
731
732 bool readUntilDoubleCRLF(char* buf, size_t buflen, int& used,
733 uint32_t timeoutMs = 3000) {
734 uint32_t start = millis();
735 used = 0;
736 int state = 0; // match \r\n\r\n
737 while ((millis() - start) < timeoutMs && used < (int)buflen - 1) {
738 int avail = m_tcp.available();
739 if (avail <= 0) {
740 delay(5);
741 continue;
742 }
743 int n = m_tcp.read((uint8_t*)buf + used, 1);
744 if (n == 1) {
745 char c = buf[used++];
746 switch (state) {
747 case 0:
748 state = (c == '\r') ? 1 : 0;
749 break;
750 case 1:
751 state = (c == '\n') ? 2 : 0;
752 break;
753 case 2:
754 state = (c == '\r') ? 3 : 0;
755 break;
756 case 3:
757 state = (c == '\n') ? 4 : 0;
758 break;
759 }
760 if (state == 4) {
761 buf[used] = '\0';
762 return true;
763 }
764 }
765 }
766 buf[used] = '\0';
767 return false;
768 }
769
770 int readExact(uint8_t* out, int len, uint32_t timeoutMs) {
771 uint32_t start = millis();
772 int got = 0;
773 while (got < len && (millis() - start) < timeoutMs) {
774 int a = m_tcp.available();
775 if (a <= 0) {
776 delay(5);
777 continue;
778 }
779 int n = m_tcp.read(out + got, len - got);
780 if (n > 0) got += n;
781 }
782 return (got == len) ? got : got; // partial OK for DESCRIBE
783 }
784
785 static int parseContentLength(const char* headers) {
786 const char* p = strcasestr(headers, "Content-Length:");
787 if (!p) return 0;
788 int len = 0;
789 if (sscanf(p, "Content-Length: %d", &len) == 1) return len;
790 return 0;
791 }
792
793 void parseSessionFromHeaders(const char* headers) {
794 const char* p = strcasestr(headers, "Session:");
795 if (!p) return;
796 p += 8; // skip "Session:"
797 while (*p == ' ' || *p == '\t') ++p;
798 size_t i = 0;
799 while (*p && *p != '\r' && *p != '\n' && *p != ';' &&
800 i < sizeof(m_sessionId) - 1) {
801 m_sessionId[i++] = *p++;
802 }
803 m_sessionId[i] = '\0';
804 }
805
806 void parseServerPortsFromHeaders(const char* headers) {
807 const char* t = strcasestr(headers, "Transport:");
808 if (!t) return;
809 const char* s = strcasestr(t, "server_port=");
810 if (!s) return;
811 s += strlen("server_port=");
812 int a = 0, b = 0;
813 if (sscanf(s, "%d-%d", &a, &b) == 2) {
814 m_serverRtpPort = (uint16_t)a;
815 }
816 }
817
818 // --- SDP parsing (rtpmap) ---
819 void parseSdp(const char* sdp) {
820 if (!sdp) return;
821 const char* p = sdp;
822 while ((p = strcasestr(p, "a=rtpmap:")) != nullptr) {
823 p += 9; // after a=rtpmap:
824 int pt = 0;
825 if (sscanf(p, "%d", &pt) != 1) continue;
826 const char* space = strchr(p, ' ');
827 if (!space) continue;
828 ++space;
829 // encoding up to '/' or endline
830 size_t i = 0;
831 while (space[i] && space[i] != '/' && space[i] != '\r' &&
832 space[i] != '\n' && i < sizeof(m_encoding) - 1) {
833 m_encoding[i] = space[i];
834 ++i;
835 }
836 m_encoding[i] = '\0';
837 int rate = 0, ch = 0;
838 const char* afterEnc = space + i;
839 if (*afterEnc == '/') {
840 ++afterEnc;
841 if (sscanf(afterEnc, "%d/%d", &rate, &ch) < 1) {
842 rate = 0;
843 ch = 0;
844 }
845 }
846 m_payloadType = (uint8_t)pt;
847 // Fill AudioInfo only for raw PCM encodings
848 if (strcasecmp(m_encoding, "L16") == 0) {
849 m_info = AudioInfo(rate, (ch > 0 ? ch : (ch == 0 ? 1 : ch)), 16);
850 } else if (strcasecmp(m_encoding, "L8") == 0) {
851 m_info = AudioInfo(rate, (ch > 0 ? ch : (ch == 0 ? 1 : ch)), 8);
852 } else {
853 m_info = AudioInfo();
854 }
856
857 return; // first match
858 }
859 }
860
861 // --- Content-Base header parsing ---
862 void parseContentBaseFromHeaders(const char* headers) {
863 m_contentBase[0] = '\0';
864 if (!headers) return;
865 const char* p = strcasestr(headers, "Content-Base:");
866 if (!p) return;
867 p += strlen("Content-Base:");
868 while (*p == ' ' || *p == '\t') ++p;
869 size_t i = 0;
870 while (*p && *p != '\r' && *p != '\n' && i < sizeof(m_contentBase) - 1) {
871 m_contentBase[i++] = *p++;
872 }
873 m_contentBase[i] = '\0';
874 // Ensure trailing '/'
875 if (i > 0 && m_contentBase[i - 1] != '/') {
876 if (i + 1 < sizeof(m_contentBase)) {
877 m_contentBase[i++] = '/';
878 m_contentBase[i] = '\0';
879 }
880 }
881 }
882
883 // --- SDP control parsing ---
884 void parseControlFromSdp(const char* sdp) {
885 m_sdpControl[0] = '\0';
886 if (!sdp) return;
887 const char* audio = strcasestr(sdp, "\nm=audio ");
888 const char* searchStart = sdp;
889 const char* searchEnd = nullptr;
890 if (audio) {
891 // find end of this media block (next m= or end)
892 searchStart = audio;
893 const char* nextm = strcasestr(audio + 1, "\nm=");
894 searchEnd = nextm ? nextm : (sdp + strlen(sdp));
895 } else {
896 // fall back to session-level
897 searchStart = sdp;
898 searchEnd = sdp + strlen(sdp);
899 }
900 const char* p = searchStart;
901 while (p && p < searchEnd) {
902 const char* ctrl = strcasestr(p, "a=control:");
903 if (!ctrl || ctrl >= searchEnd) break;
904 ctrl += strlen("a=control:");
905 // copy value until CR/LF
906 size_t i = 0;
907 while (ctrl[i] && ctrl[i] != '\r' && ctrl[i] != '\n' &&
908 i < sizeof(m_sdpControl) - 1) {
909 m_sdpControl[i] = ctrl[i];
910 ++i;
911 }
912 m_sdpControl[i] = '\0';
913 break;
914 }
915 }
916
917 bool isAbsoluteRtspUrl(const char* url) {
918 if (!url) return false;
919 return (strncasecmp(url, "rtsp://", 7) == 0) ||
920 (strncasecmp(url, "rtsps://", 8) == 0);
921 }
922
924 // default fallback if no control provided
925 if (m_sdpControl[0] == '\0') {
926 snprintf(m_trackUrl, sizeof(m_trackUrl), "%strackID=0", m_baseUrl);
927 return;
928 }
930 strncpy(m_trackUrl, m_sdpControl, sizeof(m_trackUrl) - 1);
931 m_trackUrl[sizeof(m_trackUrl) - 1] = '\0';
932 return;
933 }
934 const char* base = (m_contentBase[0] ? m_contentBase : m_baseUrl);
935 size_t blen = strlen(base);
936 // Construct base ensuring single '/'
937 char tmp[256];
938 size_t pos = 0;
939 for (; pos < sizeof(tmp) - 1 && pos < blen; ++pos) tmp[pos] = base[pos];
940 if (pos > 0 && tmp[pos - 1] != '/' && pos < sizeof(tmp) - 1)
941 tmp[pos++] = '/';
942 // If control starts with '/', skip one to avoid '//'
943 const char* ctrl = m_sdpControl;
944 if (*ctrl == '/') ++ctrl;
945 while (*ctrl && pos < sizeof(tmp) - 1) tmp[pos++] = *ctrl++;
946 tmp[pos] = '\0';
947 strncpy(m_trackUrl, tmp, sizeof(m_trackUrl) - 1);
948 m_trackUrl[sizeof(m_trackUrl) - 1] = '\0';
949 }
950
951 // resampler is started in constructor; audio info will be set dynamically
952};
953
954} // namespace audio_tools
If you want to use the framework w/o Arduino you need to provide the implementation of a couple of cl...
#define LOGW(...)
Definition AudioLoggerIDF.h:29
#define LOGI(...)
Definition AudioLoggerIDF.h:28
#define LOGD(...)
Definition AudioLoggerIDF.h:27
#define LOGE(...)
Definition AudioLoggerIDF.h:30
Definition Arduino.h:56
Decoding of encoded audio into PCM data.
Definition AudioCodecsBase.h:19
Print * getOutput()
Definition AudioCodecsBase.h:68
void setAudioInfo(AudioInfo from) override
for most decoders this is not needed
Definition AudioCodecsBase.h:32
virtual const char * mime()=0
Provides the mime type of the data that is expected by this decoder.
AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition AudioCodecsBase.h:29
Supports the subscription to audio change notifications.
Definition AudioTypes.h:152
bool isNotifyActive()
Checks if the automatic AudioInfo update is active.
Definition AudioTypes.h:174
virtual void addNotifyAudioChange(AudioInfoSupport &bi)
Adds target to be notified about audio changes.
Definition AudioTypes.h:155
void setNotifyActive(bool flag)
Deactivate/Reactivate automatic AudioInfo updates: (default is active)
Definition AudioTypes.h:171
virtual bool removeNotifyAudioChange(AudioInfoSupport &bi)
Removes a target in order not to be notified about audio changes.
Definition AudioTypes.h:160
virtual void clearNotifyAudioChange()
Deletes all change notify subscriptions.
Definition AudioTypes.h:168
Supports changes to the sampling rate, bits and channels.
Definition AudioTypes.h:137
Abstract Audio Ouptut class.
Definition AudioOutput.h:25
Base class for all Audio Streams. It support the boolean operator to test if the object is ready with...
Definition BaseStream.h:120
void clear()
same as reset
Definition Buffers.h:96
bool isEmpty()
Definition Buffers.h:87
DecoderL8 - Converts an 8 Bit Stream into 16Bits Most microcontrollers can not output 8 bit data dire...
Definition CodecL8.h:19
PCM decoder which converts from network format to the host format.
Definition CodecNetworkFormat.h:18
Arduino-compatible IPAddress class implemented in pure C++.
Definition IPAddress.h:28
Manage multiple AudioDecoders with automatic format detection.
Definition MultiDecoder.h:51
void setOutput(Print &out_stream) override
Sets the output stream for decoded audio data.
Definition MultiDecoder.h:151
bool selectDecoder(const char *mime)
Definition MultiDecoder.h:196
size_t write(const uint8_t *data, size_t len) override
Writes encoded audio data to be decoded.
Definition MultiDecoder.h:281
bool begin() override
Starts the processing and enables automatic MIME type determination.
Definition MultiDecoder.h:99
void addDecoder(AudioDecoder &decoder, const char *mime=nullptr)
Adds a decoder that will be selected by its MIME type.
Definition MultiDecoder.h:135
Efficient RTSP client for UDP/RTP audio with decoder pipeline.
Definition RTSPClient.h:47
bool readUntilDoubleCRLF(char *buf, size_t buflen, int &used, uint32_t timeoutMs=3000)
Definition RTSPClient.h:732
bool m_udp_active
Definition RTSPClient.h:472
char m_sdpControl[128]
Definition RTSPClient.h:481
void setHeaderTimeoutMs(uint32_t ms)
Set timeout (ms) for reading RTSP response headers. Increase if your server responds slowly....
Definition RTSPClient.h:119
size_t computeRtpPayloadOffset(const uint8_t *data, size_t length)
Definition RTSPClient.h:587
void setConnectRetries(uint8_t retries)
Set number of TCP connect retries (default 2).
Definition RTSPClient.h:108
void addNotifyAudioChange(AudioInfoSupport &bi) override
Adds target to be notified about audio changes.
Definition RTSPClient.h:456
uint32_t m_cseq
Definition RTSPClient.h:477
bool fail(const char *msg)
Definition RTSPClient.h:562
RTSPClient(AudioOutput &out)
Construct with an AudioOutput as decoding sink.
Definition RTSPClient.h:62
MultiDecoder m_multi_decoder
Definition RTSPClient.h:497
bool m_started
Definition RTSPClient.h:485
UdpSocket m_udp
Definition RTSPClient.h:471
float m_resampleStep
Definition RTSPClient.h:509
void setPayloadOffset(uint8_t bytes)
Set additional RTP payload offset in bytes. Some payloads embed a small header before the actual audi...
Definition RTSPClient.h:127
const uint32_t m_keepaliveIntervalMs
Definition RTSPClient.h:488
bool isAbsoluteRtspUrl(const char *url)
Definition RTSPClient.h:917
void parseSessionFromHeaders(const char *headers)
Definition RTSPClient.h:793
const char * mime() const
Best-effort MIME derived from SDP (e.g. audio/L16, audio/aac).
Definition RTSPClient.h:305
bool begin(IPAddress addr, uint16_t port, const char *path=nullptr)
Start RTSP session and UDP RTP reception.
Definition RTSPClient.h:137
RTSPClient(AudioStream &out)
Construct with an AudioStream as decoding sink.
Definition RTSPClient.h:66
IPAddress m_addr
Definition RTSPClient.h:473
int available()
Returns buffered RTP payload bytes available for copy().
Definition RTSPClient.h:284
bool tcpCommit()
Definition RTSPClient.h:671
bool isNotifyActive()
Definition RTSPClient.h:466
uint8_t m_payloadType
Definition RTSPClient.h:513
void setOutput(AudioOutput &out)
Define decoding sink as AudioOutput.
Definition RTSPClient.h:74
bool removeNotifyAudioChange(AudioInfoSupport &bi) override
Removes a target in order not to be notified about audio changes.
Definition RTSPClient.h:459
uint16_t m_clientRtpPort
Definition RTSPClient.h:483
uint32_t m_headerTimeoutMs
Definition RTSPClient.h:505
uint32_t m_connectRetryDelayMs
Definition RTSPClient.h:504
RTSPClient(Print &out)
Construct with a generic Print sink.
Definition RTSPClient.h:70
char m_sessionId[64]
Definition RTSPClient.h:482
bool setActive(bool active)
Pause or resume playback via RTSP PAUSE/PLAY.
Definition RTSPClient.h:354
char m_trackUrl[128]
Definition RTSPClient.h:479
void buildUrls(const char *path)
Definition RTSPClient.h:528
SingleBuffer< uint8_t > m_pktBuf
Definition RTSPClient.h:491
void setIdleDelay(uint32_t ms)
Set idle backoff delay (ms) for zero-return cases. Used in available() and copy() to avoid busy loops...
Definition RTSPClient.h:103
AudioInfo m_info
Definition RTSPClient.h:515
void resetState()
Definition RTSPClient.h:517
void parseSdp(const char *sdp)
Definition RTSPClient.h:819
void setConnectRetryDelayMs(uint32_t ms)
Set delay between connect retries in ms (default 500ms).
Definition RTSPClient.h:113
void parseControlFromSdp(const char *sdp)
Definition RTSPClient.h:884
void maybeKeepalive()
Definition RTSPClient.h:568
char m_bodyBuf[1024]
Definition RTSPClient.h:494
void parseContentBaseFromHeaders(const char *headers)
Definition RTSPClient.h:862
void primeUdpPath()
Definition RTSPClient.h:637
void setNotifyActive(bool flag)
Definition RTSPClient.h:465
void serviceUdp()
Definition RTSPClient.h:597
uint8_t m_connectRetries
Definition RTSPClient.h:503
RTSPClient()
Definition RTSPClient.h:49
char m_hdrBuf[1024]
Definition RTSPClient.h:493
DecoderNetworkFormat m_decoder_net
Definition RTSPClient.h:498
void end()
Stop streaming and close RTSP/UDP sockets.
Definition RTSPClient.h:258
uint16_t m_serverRtpPort
Definition RTSPClient.h:484
uint8_t payloadType() const
RTP payload type from SDP (0xFF if unknown).
Definition RTSPClient.h:347
uint32_t m_lastKeepaliveMs
Definition RTSPClient.h:487
void parseServerPortsFromHeaders(const char *headers)
Definition RTSPClient.h:806
bool sniffUdpFor(uint32_t ms)
Definition RTSPClient.h:651
char m_encoding[32]
Definition RTSPClient.h:514
uint32_t m_idleDelayMs
Definition RTSPClient.h:501
uint16_t m_port
Definition RTSPClient.h:474
bool m_decoderReady
Definition RTSPClient.h:500
void setAudioInfo(AudioInfo info) override
Defines the input AudioInfo.
Definition RTSPClient.h:451
char m_contentBase[160]
Definition RTSPClient.h:480
void buildTrackUrlFromBaseAndControl()
Definition RTSPClient.h:923
void setResampleFactor(float factor)
Set resampling factor to stabilize buffers and playback. 1.0 means no resampling. factor > 1....
Definition RTSPClient.h:92
TcpClient m_tcp
Definition RTSPClient.h:470
static int parseContentLength(const char *headers)
Definition RTSPClient.h:785
uint8_t m_payloadOffset
Definition RTSPClient.h:502
ResampleStream m_resampler
Definition RTSPClient.h:508
void setOutput(Print &out)
Define decoding sink as Print.
Definition RTSPClient.h:82
void clearNotifyAudioChange() override
Deletes all change notify subscriptions.
Definition RTSPClient.h:462
void setOutput(AudioStream &out)
Define decoding sink as AudioStream.
Definition RTSPClient.h:78
bool openUdpPorts()
Definition RTSPClient.h:549
size_t copy()
Copy the next buffered RTP payload into the decoder pipeline. Performs initial decoder selection base...
Definition RTSPClient.h:400
AudioInfo audioInfo() override
Audio info parsed from SDP for raw PCM encodings.
Definition RTSPClient.h:449
int readExact(uint8_t *out, int len, uint32_t timeoutMs)
Definition RTSPClient.h:770
DecoderL8 m_decoder_l8
Definition RTSPClient.h:499
bool m_isPlaying
Definition RTSPClient.h:486
void addDecoder(AudioDecoder &decoder)
Register a decoder to be auto-selected for its own MIME type.
Definition RTSPClient.h:391
size_t tcpWrite(const uint8_t *data, size_t len)
Definition RTSPClient.h:666
char m_baseUrl[96]
Definition RTSPClient.h:478
SingleBuffer< uint8_t > m_tcpCmd
Definition RTSPClient.h:492
void addDecoder(const char *mimeType, AudioDecoder &decoder)
Register a decoder to be auto-selected for the given MIME.
Definition RTSPClient.h:383
bool sendSimpleRequest(const char *method, const char *url, const char *extraHeaders, size_t extraLen, char *outHeaders, size_t outHeadersLen, char *outBody, size_t outBodyLen, bool quiet=false)
Definition RTSPClient.h:678
virtual void setStream(Stream &stream) override
Defines/Changes the input & output.
Definition AudioIO.h:270
virtual void setOutput(AudioOutput &print)
Defines/Changes the output target and registers for audio change notifications.
Definition AudioIO.h:284
Dynamic Resampling. We can use a variable factor to speed up or slow down the playback.
Definition ResampleStream.h:33
void setStepSize(float step)
influence the sample rate
Definition ResampleStream.h:154
bool begin(ResampleConfig cfg)
Definition ResampleStream.h:63
A simple Buffer implementation which just uses a (dynamically sized) array.
Definition Buffers.h:194
size_t size() override
Definition Buffers.h:325
size_t setAvailable(size_t available_size)
Definition Buffers.h:318
int available() override
provides the number of entries that are available to read
Definition Buffers.h:255
int writeArray(const T data[], int len) override
Fills the buffer data.
Definition Buffers.h:223
T * data()
Provides address of actual data.
Definition Buffers.h:306
bool resize(size_t size)
Resizes the buffer if supported: returns false if not supported.
Definition Buffers.h:327
int clearArray(int len) override
consumes len bytes and moves current data to the beginning
Definition Buffers.h:274
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
void delay(uint32_t ms)
Definition Arduino.h:259
uint32_t millis()
Returns the milliseconds since the start.
Definition Arduino.h:260
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:56