arduino-audio-tools
Loading...
Searching...
No Matches
RTSPClient.h
1#pragma once
2#include <Arduino.h>
3#include <IPAddress.h>
4
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"
11
12namespace audio_tools {
13
38template <typename TcpClient, typename UdpSocket>
40 public:
41 RTSPClient() {
42 // convert network format to little endian
43 m_multi_decoder.addDecoder(m_decoder_net, "audio/L16");
44 // convert to 16 bit
45 m_multi_decoder.addDecoder(m_decoder_l8, "audio/L8");
46 // Start resampler; it will receive AudioInfo later via setAudioInfo
47 m_resampler.begin();
48 m_multi_decoder.setOutput(m_resampler);
49 }
50
66 void setOutput(AudioOutput& out) { m_resampler.setOutput(out); }
70 void setOutput(AudioStream& out) { m_resampler.setStream(out); }
74 void setOutput(Print& out) { m_resampler.setOutput(out); }
75
84 void setResampleFactor(float factor) {
85 if (factor <= 0.0f) factor = 1.0f;
86 float step = 1.0f / factor;
87 m_resampleStep = step;
88 m_resampler.setStepSize(step);
89 // Always route via resampler; factor 1.0 is pass-through
90 }
95 void setIdleDelay(uint32_t ms) { m_idleDelayMs = ms; }
96
100 void setConnectRetries(uint8_t retries) { m_connectRetries = retries; }
101
105 void setConnectRetryDelayMs(uint32_t ms) { m_connectRetryDelayMs = ms; }
106
111 void setHeaderTimeoutMs(uint32_t ms) { m_headerTimeoutMs = ms; }
112
119 void setPayloadOffset(uint8_t bytes) { m_payloadOffset = bytes; }
129 bool begin(IPAddress addr, uint16_t port, const char* path = nullptr) {
130 resetState();
131 m_addr = addr;
132 m_port = port;
133
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);
137 // m_tcp.setTimeout(m_headerTimeoutMs / 1000);
138 bool connected = false;
139 for (uint8_t attempt = 0; attempt <= m_connectRetries; ++attempt) {
140 if (m_tcp.connect(m_addr, m_port)) {
141 connected = true;
142 break;
143 }
144 LOGW("RTSPClient: connect attempt %u failed", (unsigned)(attempt + 1));
145 if (attempt < m_connectRetries) delay(m_connectRetryDelayMs);
146 }
147 if (!connected) {
148 LOGE("RTSPClient: TCP connect failed");
149 return false;
150 }
151 m_tcp.setNoDelay(true);
152
153 // Build base URL and track URL
154 buildUrls(path);
155
156 // CSeq starts at 1
157 m_cseq = 1;
158
159 // OPTIONS
160 LOGI("OPTIONS");
161 int retry = m_connectRetries;
162 while (!sendSimpleRequest("OPTIONS", m_baseUrl, nullptr, 0, m_hdrBuf,
163 sizeof(m_hdrBuf), nullptr, 0)) {
164 if (--retry == 0) {
165 return fail("OPTIONS failed");
166 } else {
167 LOGW("RTSPClient: retrying OPTIONS");
168 delay(800);
169 }
170 }
171
172 // DESCRIBE
173 LOGI("DESCRIBE");
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");
179
180 // Parse SDP (rtpmap) to capture payload and encoding
181 parseSdp(m_bodyBuf);
182 // Parse Content-Base for absolute/relative control resolution
183 parseContentBaseFromHeaders(m_hdrBuf);
184 // Parse a=control and build the correct track URL for SETUP
185 parseControlFromSdp(m_bodyBuf);
186 buildTrackUrlFromBaseAndControl();
187 LOGI("RTSPClient: SDP control='%s' content-base='%s'", m_sdpControl,
188 m_contentBase);
189 LOGI("RTSPClient: SETUP url: %s", m_trackUrl);
190
191 // Prepare UDP (client_port)
192 if (!openUdpPorts()) return fail("UDP bind failed");
193
194 // SETUP with client_port pair
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),
201 nullptr, 0)) {
202 // Fallback: some servers require explicit UDP in transport profile
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),
208 nullptr, 0)) {
209 return fail("SETUP failed");
210 }
211 }
212
213 // Parse Session and server_port from last headers
214 parseSessionFromHeaders(m_hdrBuf);
215 parseServerPortsFromHeaders(m_hdrBuf);
216 if (m_sessionId[0] == '\0') return fail("Missing Session ID");
217
218 // Prime UDP path to server RTP port (helps some networks/servers)
219 primeUdpPath();
220
221 // PLAY
222 LOGI("PLAY");
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)) {
227 // Some servers start streaming RTP immediately but delay/omit PLAY
228 // response Treat PLAY as successful if RTP arrives shortly after sending
229 // PLAY
230 if (sniffUdpFor(1500)) {
231 LOGW("RTSPClient: proceeding without PLAY response (RTP detected)");
232 } else {
233 return fail("PLAY failed");
234 }
235 }
236
237 m_started = true;
238 m_isPlaying = true;
239 m_lastKeepaliveMs = millis();
240 return true;
241 }
242
245 operator bool() { return m_started && mime() != nullptr && available() > 0; }
246
250 void end() {
251 if (m_started) {
252 // best-effort TEARDOWN
253 if (m_tcp.connected()) {
254 char sessionHdr[128];
255 if (m_sessionId[0]) {
256 snprintf(sessionHdr, sizeof(sessionHdr), "Session: %s\r\n",
257 m_sessionId);
258 sendSimpleRequest("TEARDOWN", m_baseUrl, sessionHdr,
259 strlen(sessionHdr), m_hdrBuf, sizeof(m_hdrBuf),
260 nullptr, 0, /*quiet*/ true);
261 }
262 }
263 }
264
265 if (m_udp_active) {
266 m_udp.stop();
267 }
268 if (m_tcp.connected()) m_tcp.stop();
269 m_started = false;
270 m_isPlaying = false;
271 }
272
276 int available() {
277 if (!m_started) {
278 delay(m_idleDelayMs);
279 return 0;
280 }
281 // keepalive regardless of play state
282 maybeKeepalive();
283 if (!m_isPlaying) {
284 delay(m_idleDelayMs);
285 return 0;
286 }
287 serviceUdp();
288 int avail = m_pktBuf.available();
289 if (avail == 0) delay(m_idleDelayMs);
290 return avail;
291 }
292
297 const char* mime() const {
298 // Prefer static RTP payload type mapping when available
299 switch (m_payloadType) {
300 case 0: // PCMU
301 return "audio/PCMU";
302 case 3: // GSM
303 return "audio/gsm";
304 case 4: // G723
305 return "audio/g723";
306 case 5: // DVI4/8000 (IMA ADPCM)
307 case 6: // DVI4/16000
308 case 16: // DVI4/11025
309 case 17: // DVI4/22050
310 return "audio/adpcm";
311 case 8: // PCMA
312 return "audio/PCMA";
313 case 9: // G722
314 return "audio/g722";
315 case 10: // L16 stereo
316 case 11: // L16 mono
317 return "audio/L16";
318 case 14: // MPA (MPEG audio / MP3)
319 return "audio/mpeg";
320 default:
321 break; // dynamic or unknown; fall back to SDP encoding string
322 }
323 // Fallback: infer from SDP encoding token
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"; // MP3
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"; // IMA ADPCM
333 return nullptr;
334 }
335
339 uint8_t payloadType() const { return m_payloadType; }
340
346 bool setActive(bool active) {
347 if (!m_started || !m_tcp.connected() || m_sessionId[0] == '\0')
348 return false;
349 if (active == m_isPlaying) return true; // no-op
350
351 char sessionHdr[128];
352 snprintf(sessionHdr, sizeof(sessionHdr), "Session: %s\r\n", m_sessionId);
353 bool ok;
354 if (active) {
355 ok = sendSimpleRequest("PLAY", m_baseUrl, sessionHdr, strlen(sessionHdr),
356 m_hdrBuf, sizeof(m_hdrBuf), nullptr, 0);
357 if (ok) m_isPlaying = true;
358 } else {
359 ok = sendSimpleRequest("PAUSE", m_baseUrl, sessionHdr, strlen(sessionHdr),
360 m_hdrBuf, sizeof(m_hdrBuf), nullptr, 0);
361 if (ok) {
362 m_isPlaying = false;
363 // drop any buffered payload
364 m_pktBuf.clear();
365 }
366 }
367 return ok;
368 }
369
375 void addDecoder(const char* mimeType, AudioDecoder& decoder) {
376 m_multi_decoder.addDecoder(decoder, mimeType);
377 }
378
384 size_t copy() {
385 if (!m_started) {
386 delay(m_idleDelayMs);
387 LOGD("not started");
388 return 0;
389 }
390
391 maybeKeepalive();
392
393 if (!m_isPlaying) {
394 delay(m_idleDelayMs);
395 LOGD("not playing");
396 return 0;
397 }
398
399 serviceUdp();
400
401 if (m_pktBuf.isEmpty()) {
402 LOGD("no data");
403 delay(m_idleDelayMs);
404 return 0;
405 }
406
407 // On first data, make sure decoder selection and audio info are applied
408 if (!m_decoderReady) {
409 const char* m = mime();
410 if (m) {
411 LOGI("Selecting decoder: %s", m);
412 // Ensure network format decoder has correct PCM info
413 m_multi_decoder.selectDecoder(m);
414 m_multi_decoder.setAudioInfo(m_info);
415 if (m_multi_decoder.getOutput() != nullptr) {
416 m_multi_decoder.begin(); // start decoder only when output is defined
417 }
418 m_decoderReady = true;
419 }
420 }
421
422 int n = m_pktBuf.available();
423 size_t written = m_multi_decoder.write(m_pktBuf.data(), n);
424 LOGI("copy: %d -> %d", (int)n, (int)written);
425 m_pktBuf.clearArray(written);
426 return written;
427 }
428
433 AudioInfo audioInfo() override { return m_multi_decoder.audioInfo(); }
434
435 void setAudioInfo(AudioInfo info) override {
436 m_multi_decoder.setAudioInfo(info);
437 }
438
439 // AudioInfoSource forwarding: delegate notifications to MultiDecoder
441 m_multi_decoder.addNotifyAudioChange(bi);
442 }
444 return m_multi_decoder.removeNotifyAudioChange(bi);
445 }
446 void clearNotifyAudioChange() override {
447 m_multi_decoder.clearNotifyAudioChange();
448 }
449 void setNotifyActive(bool flag) { m_multi_decoder.setNotifyActive(flag); }
450 bool isNotifyActive() { return m_multi_decoder.isNotifyActive(); }
451
452 protected:
453 // Connection
454 TcpClient m_tcp;
455 UdpSocket m_udp;
456 bool m_udp_active = false;
457 IPAddress m_addr{};
458 uint16_t m_port = 0;
459
460 // RTSP state
461 uint32_t m_cseq = 1;
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; // even
468 uint16_t m_serverRtpPort = 0; // optional from Transport response
469 bool m_started = false;
470 bool m_isPlaying = false;
471 uint32_t m_lastKeepaliveMs = 0;
472 const uint32_t m_keepaliveIntervalMs = 25000; // 25s
473
474 // Buffers
475 SingleBuffer<uint8_t> m_pktBuf{0};
476 SingleBuffer<uint8_t> m_tcpCmd{0};
477 char m_hdrBuf[1024];
478 char m_bodyBuf[1024];
479
480 // Decoder pipeline
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; // extra bytes after RTP header/CSRCs
487 uint8_t m_connectRetries = 2;
488 uint32_t m_connectRetryDelayMs = 500;
489 uint32_t m_headerTimeoutMs = 4000; // header read timeout
490
491 // Resampling pipeline
492 ResampleStream m_resampler;
493 float m_resampleStep = 1.0f;
494 // Sinks are set directly on the resampler
495
496 // --- RTP/SDP fields ---
497 uint8_t m_payloadType = 0xFF; // unknown by default
498 char m_encoding[32] = {0};
499 AudioInfo m_info{0, 0, 0};
500
501 void resetState() {
502 m_sessionId[0] = '\0';
503 m_serverRtpPort = 0;
504 m_clientRtpPort = 0;
505 m_cseq = 1;
506 m_pktBuf.resize(2048);
507 m_pktBuf.clear();
508 m_decoderReady = false;
509 m_udp_active = false;
510 }
511
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);
515 if (path && *path) {
516 const char* p = path;
517 if (*p == '/') ++p; // skip leading slash
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);
521 // ensure trailing '/'
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';
527 }
528 }
529 }
530 snprintf(m_trackUrl, sizeof(m_trackUrl), "%strackID=0", m_baseUrl);
531 }
532
533 bool openUdpPorts() {
534 // Try a few even RTP ports starting at 5004
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);
538 m_clientRtpPort = p;
539 m_udp_active = true;
540 return true;
541 }
542 }
543 return false;
544 }
545
546 bool fail(const char* msg) {
547 LOGE("RTSPClient: %s", msg);
548 end();
549 return false;
550 }
551
552 void maybeKeepalive() {
553 if (!m_started || !m_tcp.connected()) return;
554 uint32_t now = millis();
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, /*quiet*/ true);
562 } else {
563 sendSimpleRequest("OPTIONS", m_baseUrl, nullptr, 0, m_hdrBuf,
564 sizeof(m_hdrBuf), nullptr, 0, /*quiet*/ true);
565 }
566 }
567
568 // Compute the RTP payload offset inside a UDP packet
569 // Considers fixed RTP header (12 bytes), CSRC count, and configured extra
570 // offset
571 size_t computeRtpPayloadOffset(const uint8_t* data, size_t length) {
572 if (length <= 12) return length;
573 size_t offset = 12;
574 uint8_t cc = data[0] & 0x0F; // CSRC count
575 offset += cc * 4;
576 // Apply any configured additional payload offset (e.g., RFC2250)
577 offset += m_payloadOffset;
578 return offset;
579 }
580
581 void serviceUdp() {
582 // Keep RTSP session alive
583 maybeKeepalive();
584
585 if (!m_udp_active) {
586 LOGE("no UDP");
587 return;
588 }
589 if (m_pktBuf.available() > 0) {
590 LOGI("Still have unprocessed data");
591 return; // still have data buffered
592 }
593
594 // parse next UDP packet
595 int packetSize = m_udp.parsePacket();
596 if (packetSize <= 0) {
597 LOGD("packet size: %d", packetSize);
598 return;
599 }
600
601 // Fill buffer
602 if ((size_t)packetSize > m_pktBuf.size()) m_pktBuf.resize(packetSize);
603 int n = m_udp.read(m_pktBuf.data(), packetSize);
604 m_pktBuf.setAvailable(n);
605 if (n <= 12) {
606 LOGE("packet too small: %d", n);
607 return; // too small to contain RTP
608 }
609
610 // Very basic RTP parsing: compute payload offset
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);
615 }
616
617 // move payload to beginning for contiguous read
618 m_pktBuf.clearArray(payloadOffset);
619 }
620
621 void primeUdpPath() {
622 if (!m_udp_active) return;
623 if (m_serverRtpPort == 0) return;
624 // Send a tiny datagram to server RTP port to open NAT/flows
625 // Not required by RTSP, but improves interoperability
626 for (int i = 0; i < 2; ++i) {
627 m_udp.beginPacket(m_addr, m_serverRtpPort);
628 uint8_t b = 0x00;
629 m_udp.write(&b, 1);
630 m_udp.endPacket();
631 delay(2);
632 }
633 }
634
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) {
641 // restore to be processed by normal path
642 return true;
643 }
644 delay(5);
645 }
646 return false;
647 }
648
649 // Centralized TCP write helper
650 size_t tcpWrite(const uint8_t* data, size_t len) {
651 if (m_tcpCmd.size() < 400) m_tcpCmd.resize(400);
652 return m_tcpCmd.writeArray(data, len);
653 }
654
655 bool tcpCommit() {
656 bool rc = m_tcp.write(m_tcpCmd.data(), m_tcpCmd.available()) ==
657 m_tcpCmd.available();
658 m_tcpCmd.clear();
659 return rc;
660 }
661
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) {
666 // Build request
667 char reqStart[256];
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;
673
674 // Send start line + mandatory headers
675 if (tcpWrite((const uint8_t*)reqStart, reqLen) != (size_t)reqLen) {
676 return false;
677 }
678 // Optional extra headers
679 if (extraHeaders && extraLen) {
680 if (tcpWrite((const uint8_t*)extraHeaders, extraLen) != extraLen) {
681 return false;
682 }
683 }
684 // End of headers
685 const char* end = "\r\n";
686 if (tcpWrite((const uint8_t*)end, 2) != 2) {
687 return false;
688 }
689
690 if (!tcpCommit()) {
691 LOGE("TCP write failed");
692 return false;
693 }
694
695 // Read response headers until CRLFCRLF
696 int hdrUsed = 0;
697 memset(outHeaders, 0, outHeadersLen);
698 if (!readUntilDoubleCRLF(outHeaders, outHeadersLen, hdrUsed,
699 m_headerTimeoutMs)) {
700 if (!quiet) LOGE("RTSPClient: header read timeout");
701 return false;
702 }
703
704 // Optionally read body based on Content-Length
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;
711 outBody[got] = '\0';
712 }
713 return true;
714 }
715
716 bool readUntilDoubleCRLF(char* buf, size_t buflen, int& used,
717 uint32_t timeoutMs = 3000) {
718 uint32_t start = millis();
719 used = 0;
720 int state = 0; // match \r\n\r\n
721 while ((millis() - start) < timeoutMs && used < (int)buflen - 1) {
722 int avail = m_tcp.available();
723 if (avail <= 0) {
724 delay(5);
725 continue;
726 }
727 int n = m_tcp.read((uint8_t*)buf + used, 1);
728 if (n == 1) {
729 char c = buf[used++];
730 switch (state) {
731 case 0:
732 state = (c == '\r') ? 1 : 0;
733 break;
734 case 1:
735 state = (c == '\n') ? 2 : 0;
736 break;
737 case 2:
738 state = (c == '\r') ? 3 : 0;
739 break;
740 case 3:
741 state = (c == '\n') ? 4 : 0;
742 break;
743 }
744 if (state == 4) {
745 buf[used] = '\0';
746 return true;
747 }
748 }
749 }
750 buf[used] = '\0';
751 return false;
752 }
753
754 int readExact(uint8_t* out, int len, uint32_t timeoutMs) {
755 uint32_t start = millis();
756 int got = 0;
757 while (got < len && (millis() - start) < timeoutMs) {
758 int a = m_tcp.available();
759 if (a <= 0) {
760 delay(5);
761 continue;
762 }
763 int n = m_tcp.read(out + got, len - got);
764 if (n > 0) got += n;
765 }
766 return (got == len) ? got : got; // partial OK for DESCRIBE
767 }
768
769 static int parseContentLength(const char* headers) {
770 const char* p = strcasestr(headers, "Content-Length:");
771 if (!p) return 0;
772 int len = 0;
773 if (sscanf(p, "Content-Length: %d", &len) == 1) return len;
774 return 0;
775 }
776
777 void parseSessionFromHeaders(const char* headers) {
778 const char* p = strcasestr(headers, "Session:");
779 if (!p) return;
780 p += 8; // skip "Session:"
781 while (*p == ' ' || *p == '\t') ++p;
782 size_t i = 0;
783 while (*p && *p != '\r' && *p != '\n' && *p != ';' &&
784 i < sizeof(m_sessionId) - 1) {
785 m_sessionId[i++] = *p++;
786 }
787 m_sessionId[i] = '\0';
788 }
789
790 void parseServerPortsFromHeaders(const char* headers) {
791 const char* t = strcasestr(headers, "Transport:");
792 if (!t) return;
793 const char* s = strcasestr(t, "server_port=");
794 if (!s) return;
795 s += strlen("server_port=");
796 int a = 0, b = 0;
797 if (sscanf(s, "%d-%d", &a, &b) == 2) {
798 m_serverRtpPort = (uint16_t)a;
799 }
800 }
801
802 // --- SDP parsing (rtpmap) ---
803 void parseSdp(const char* sdp) {
804 if (!sdp) return;
805 const char* p = sdp;
806 while ((p = strcasestr(p, "a=rtpmap:")) != nullptr) {
807 p += 9; // after a=rtpmap:
808 int pt = 0;
809 if (sscanf(p, "%d", &pt) != 1) continue;
810 const char* space = strchr(p, ' ');
811 if (!space) continue;
812 ++space;
813 // encoding up to '/' or endline
814 size_t i = 0;
815 while (space[i] && space[i] != '/' && space[i] != '\r' &&
816 space[i] != '\n' && i < sizeof(m_encoding) - 1) {
817 m_encoding[i] = space[i];
818 ++i;
819 }
820 m_encoding[i] = '\0';
821 int rate = 0, ch = 0;
822 const char* afterEnc = space + i;
823 if (*afterEnc == '/') {
824 ++afterEnc;
825 if (sscanf(afterEnc, "%d/%d", &rate, &ch) < 1) {
826 rate = 0;
827 ch = 0;
828 }
829 }
830 m_payloadType = (uint8_t)pt;
831 // Fill AudioInfo only for raw PCM encodings
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);
836 } else {
837 m_info = AudioInfo();
838 }
839 m_multi_decoder.setAudioInfo(m_info);
840
841 return; // first match
842 }
843 }
844
845 // --- Content-Base header parsing ---
846 void parseContentBaseFromHeaders(const char* headers) {
847 m_contentBase[0] = '\0';
848 if (!headers) return;
849 const char* p = strcasestr(headers, "Content-Base:");
850 if (!p) return;
851 p += strlen("Content-Base:");
852 while (*p == ' ' || *p == '\t') ++p;
853 size_t i = 0;
854 while (*p && *p != '\r' && *p != '\n' && i < sizeof(m_contentBase) - 1) {
855 m_contentBase[i++] = *p++;
856 }
857 m_contentBase[i] = '\0';
858 // Ensure trailing '/'
859 if (i > 0 && m_contentBase[i - 1] != '/') {
860 if (i + 1 < sizeof(m_contentBase)) {
861 m_contentBase[i++] = '/';
862 m_contentBase[i] = '\0';
863 }
864 }
865 }
866
867 // --- SDP control parsing ---
868 void parseControlFromSdp(const char* sdp) {
869 m_sdpControl[0] = '\0';
870 if (!sdp) return;
871 const char* audio = strcasestr(sdp, "\nm=audio ");
872 const char* searchStart = sdp;
873 const char* searchEnd = nullptr;
874 if (audio) {
875 // find end of this media block (next m= or end)
876 searchStart = audio;
877 const char* nextm = strcasestr(audio + 1, "\nm=");
878 searchEnd = nextm ? nextm : (sdp + strlen(sdp));
879 } else {
880 // fall back to session-level
881 searchStart = sdp;
882 searchEnd = sdp + strlen(sdp);
883 }
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:");
889 // copy value until CR/LF
890 size_t i = 0;
891 while (ctrl[i] && ctrl[i] != '\r' && ctrl[i] != '\n' &&
892 i < sizeof(m_sdpControl) - 1) {
893 m_sdpControl[i] = ctrl[i];
894 ++i;
895 }
896 m_sdpControl[i] = '\0';
897 break;
898 }
899 }
900
901 bool isAbsoluteRtspUrl(const char* url) {
902 if (!url) return false;
903 return (strncasecmp(url, "rtsp://", 7) == 0) ||
904 (strncasecmp(url, "rtsps://", 8) == 0);
905 }
906
907 void buildTrackUrlFromBaseAndControl() {
908 // default fallback if no control provided
909 if (m_sdpControl[0] == '\0') {
910 snprintf(m_trackUrl, sizeof(m_trackUrl), "%strackID=0", m_baseUrl);
911 return;
912 }
913 if (isAbsoluteRtspUrl(m_sdpControl)) {
914 strncpy(m_trackUrl, m_sdpControl, sizeof(m_trackUrl) - 1);
915 m_trackUrl[sizeof(m_trackUrl) - 1] = '\0';
916 return;
917 }
918 const char* base = (m_contentBase[0] ? m_contentBase : m_baseUrl);
919 size_t blen = strlen(base);
920 // Construct base ensuring single '/'
921 char tmp[256];
922 size_t pos = 0;
923 for (; pos < sizeof(tmp) - 1 && pos < blen; ++pos) tmp[pos] = base[pos];
924 if (pos > 0 && tmp[pos - 1] != '/' && pos < sizeof(tmp) - 1)
925 tmp[pos++] = '/';
926 // If control starts with '/', skip one to avoid '//'
927 const char* ctrl = m_sdpControl;
928 if (*ctrl == '/') ++ctrl;
929 while (*ctrl && pos < sizeof(tmp) - 1) tmp[pos++] = *ctrl++;
930 tmp[pos] = '\0';
931 strncpy(m_trackUrl, tmp, sizeof(m_trackUrl) - 1);
932 m_trackUrl[sizeof(m_trackUrl) - 1] = '\0';
933 }
934
935 // resampler is started in constructor; audio info will be set dynamically
936};
937
938} // namespace audio_tools
Decoding of encoded audio into PCM data.
Definition AudioCodecsBase.h:18
void setAudioInfo(AudioInfo from) override
for most decoders this is not needed
Definition AudioCodecsBase.h:28
AudioInfo audioInfo() override
provides the actual input AudioInfo
Definition AudioCodecsBase.h:25
Supports the subscription to audio change notifications.
Definition AudioTypes.h:150
bool isNotifyActive()
Checks if the automatic AudioInfo update is active.
Definition AudioTypes.h:172
virtual void addNotifyAudioChange(AudioInfoSupport &bi)
Adds target to be notified about audio changes.
Definition AudioTypes.h:153
void setNotifyActive(bool flag)
Deactivate/Reactivate automatic AudioInfo updates: (default is active)
Definition AudioTypes.h:169
virtual bool removeNotifyAudioChange(AudioInfoSupport &bi)
Removes a target in order not to be notified about audio changes.
Definition AudioTypes.h:158
virtual void clearNotifyAudioChange()
Deletes all change notify subscriptions.
Definition AudioTypes.h:166
Supports changes to the sampling rate, bits and channels.
Definition AudioTypes.h:135
Abstract Audio Ouptut class.
Definition AudioOutput.h:22
Base class for all Audio Streams. It support the boolean operator to test if the object is ready with...
Definition BaseStream.h:122
void clear()
same as reset
Definition Buffers.h:95
void setOutput(Print &out_stream) override
Sets the output stream for decoded audio data.
Definition MultiDecoder.h:151
bool selectDecoder(const char *mime)
Selects the actual decoder by MIME type.
Definition MultiDecoder.h:183
size_t write(const uint8_t *data, size_t len) override
Writes encoded audio data to be decoded.
Definition MultiDecoder.h:241
void addDecoder(AudioDecoder &decoder, const char *mime)
Adds a decoder that will be selected by its MIME type.
Definition MultiDecoder.h:119
bool begin() override
Starts the processing and enables automatic MIME type determination.
Definition MultiDecoder.h:83
Definition NoArduino.h:62
Efficient RTSP client for UDP/RTP audio with decoder pipeline.
Definition RTSPClient.h:39
void setHeaderTimeoutMs(uint32_t ms)
Set timeout (ms) for reading RTSP response headers. Increase if your server responds slowly....
Definition RTSPClient.h:111
void setConnectRetries(uint8_t retries)
Set number of TCP connect retries (default 2).
Definition RTSPClient.h:100
void addNotifyAudioChange(AudioInfoSupport &bi) override
Adds target to be notified about audio changes.
Definition RTSPClient.h:440
RTSPClient(AudioOutput &out)
Construct with an AudioOutput as decoding sink.
Definition RTSPClient.h:54
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:119
const char * mime() const
Best-effort MIME derived from SDP (e.g. audio/L16, audio/aac).
Definition RTSPClient.h:297
bool begin(IPAddress addr, uint16_t port, const char *path=nullptr)
Start RTSP session and UDP RTP reception.
Definition RTSPClient.h:129
RTSPClient(AudioStream &out)
Construct with an AudioStream as decoding sink.
Definition RTSPClient.h:58
int available()
Returns buffered RTP payload bytes available for copy().
Definition RTSPClient.h:276
void setOutput(AudioOutput &out)
Define decoding sink as AudioOutput.
Definition RTSPClient.h:66
bool removeNotifyAudioChange(AudioInfoSupport &bi) override
Removes a target in order not to be notified about audio changes.
Definition RTSPClient.h:443
RTSPClient(Print &out)
Construct with a generic Print sink.
Definition RTSPClient.h:62
bool setActive(bool active)
Pause or resume playback via RTSP PAUSE/PLAY.
Definition RTSPClient.h:346
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:95
void setConnectRetryDelayMs(uint32_t ms)
Set delay between connect retries in ms (default 500ms).
Definition RTSPClient.h:105
void end()
Stop streaming and close RTSP/UDP sockets.
Definition RTSPClient.h:250
uint8_t payloadType() const
RTP payload type from SDP (0xFF if unknown).
Definition RTSPClient.h:339
void setAudioInfo(AudioInfo info) override
Defines the input AudioInfo.
Definition RTSPClient.h:435
void setResampleFactor(float factor)
Set resampling factor to stabilize buffers and playback. 1.0 means no resampling. factor > 1....
Definition RTSPClient.h:84
void setOutput(Print &out)
Define decoding sink as Print.
Definition RTSPClient.h:74
void clearNotifyAudioChange() override
Deletes all change notify subscriptions.
Definition RTSPClient.h:446
void setOutput(AudioStream &out)
Define decoding sink as AudioStream.
Definition RTSPClient.h:70
size_t copy()
Copy the next buffered RTP payload into the decoder pipeline. Performs initial decoder selection base...
Definition RTSPClient.h:384
AudioInfo audioInfo() override
Audio info parsed from SDP for raw PCM encodings.
Definition RTSPClient.h:433
void addDecoder(const char *mimeType, AudioDecoder &decoder)
Register a decoder to be auto-selected for the given MIME.
Definition RTSPClient.h:375
virtual void setStream(Stream &stream) override
Defines/Changes the input & output.
Definition AudioIO.h:158
void setStepSize(float step)
influence the sample rate
Definition ResampleStream.h:144
size_t setAvailable(size_t available_size)
Definition Buffers.h:295
int available() override
provides the number of entries that are available to read
Definition Buffers.h:232
bool resize(int size)
Resizes the buffer if supported: returns false if not supported.
Definition Buffers.h:304
int writeArray(const T data[], int len) override
Fills the buffer data.
Definition Buffers.h:200
T * data()
Provides address of actual data.
Definition Buffers.h:283
int clearArray(int len) override
consumes len bytes and moves current data to the beginning
Definition Buffers.h:251
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition AudioCodecsBase.h:10
uint32_t millis()
Returns the milliseconds since the start.
Definition Time.h:12
Basic Audio information which drives e.g. I2S.
Definition AudioTypes.h:55