arduino-audio-tools
Loading...
Searching...
No Matches
RTSPClient.h
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
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"
19
20namespace audio_tools {
21
46template <typename TcpClient, typename UdpSocket>
48 public:
49 RTSPClient() {
50 // convert network format to little endian
51 m_multi_decoder.addDecoder(m_decoder_net, "audio/L16");
52 // convert to 16 bit
53 m_multi_decoder.addDecoder(m_decoder_l8, "audio/L8");
54 // Start resampler; it will receive AudioInfo later via setAudioInfo
55 m_resampler.begin();
56 m_multi_decoder.setOutput(m_resampler);
57 }
58
74 void setOutput(AudioOutput& out) { m_resampler.setOutput(out); }
78 void setOutput(AudioStream& out) { m_resampler.setStream(out); }
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;
96 m_resampler.setStepSize(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
113 void setConnectRetryDelayMs(uint32_t ms) { m_connectRetryDelayMs = ms; }
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));
153 if (attempt < m_connectRetries) delay(m_connectRetryDelayMs);
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
189 parseSdp(m_bodyBuf);
190 // Parse Content-Base for absolute/relative control resolution
191 parseContentBaseFromHeaders(m_hdrBuf);
192 // Parse a=control and build the correct track URL for SETUP
193 parseControlFromSdp(m_bodyBuf);
194 buildTrackUrlFromBaseAndControl();
195 LOGI("RTSPClient: SDP control='%s' content-base='%s'", m_sdpControl,
196 m_contentBase);
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
222 parseSessionFromHeaders(m_hdrBuf);
223 parseServerPortsFromHeaders(m_hdrBuf);
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;
247 m_lastKeepaliveMs = millis();
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",
265 m_sessionId);
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) {
286 delay(m_idleDelayMs);
287 return 0;
288 }
289 // keepalive regardless of play state
290 maybeKeepalive();
291 if (!m_isPlaying) {
292 delay(m_idleDelayMs);
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
392 size_t copy() {
393 if (!m_started) {
394 delay(m_idleDelayMs);
395 LOGD("not started");
396 return 0;
397 }
398
399 maybeKeepalive();
400
401 if (!m_isPlaying) {
402 delay(m_idleDelayMs);
403 LOGD("not playing");
404 return 0;
405 }
406
407 serviceUdp();
408
409 if (m_pktBuf.isEmpty()) {
410 LOGD("no data");
411 delay(m_idleDelayMs);
412 return 0;
413 }
414
415 // On first data, make sure decoder selection and audio info are applied
416 if (!m_decoderReady) {
417 const char* m = mime();
418 if (m) {
419 LOGI("Selecting decoder: %s", m);
420 // Ensure network format decoder has correct PCM info
421 m_multi_decoder.selectDecoder(m);
422 m_multi_decoder.setAudioInfo(m_info);
423 if (m_multi_decoder.getOutput() != nullptr) {
424 m_multi_decoder.begin(); // start decoder only when output is defined
425 }
426 m_decoderReady = true;
427 }
428 }
429
430 int n = m_pktBuf.available();
431 size_t written = m_multi_decoder.write(m_pktBuf.data(), n);
432 LOGI("copy: %d -> %d", (int)n, (int)written);
433 m_pktBuf.clearArray(written);
434 return written;
435 }
436
441 AudioInfo audioInfo() override { return m_multi_decoder.audioInfo(); }
442
443 void setAudioInfo(AudioInfo info) override {
444 m_multi_decoder.setAudioInfo(info);
445 }
446
447 // AudioInfoSource forwarding: delegate notifications to MultiDecoder
449 m_multi_decoder.addNotifyAudioChange(bi);
450 }
452 return m_multi_decoder.removeNotifyAudioChange(bi);
453 }
454 void clearNotifyAudioChange() override {
455 m_multi_decoder.clearNotifyAudioChange();
456 }
457 void setNotifyActive(bool flag) { m_multi_decoder.setNotifyActive(flag); }
458 bool isNotifyActive() { return m_multi_decoder.isNotifyActive(); }
459
460 protected:
461 // Connection
462 TcpClient m_tcp;
463 UdpSocket m_udp;
464 bool m_udp_active = false;
465 IPAddress m_addr{};
466 uint16_t m_port = 0;
467
468 // RTSP state
469 uint32_t m_cseq = 1;
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; // even
476 uint16_t m_serverRtpPort = 0; // optional from Transport response
477 bool m_started = false;
478 bool m_isPlaying = false;
479 uint32_t m_lastKeepaliveMs = 0;
480 const uint32_t m_keepaliveIntervalMs = 25000; // 25s
481
482 // Buffers
483 SingleBuffer<uint8_t> m_pktBuf{0};
484 SingleBuffer<uint8_t> m_tcpCmd{0};
485 char m_hdrBuf[1024];
486 char m_bodyBuf[1024];
487
488 // Decoder pipeline
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; // extra bytes after RTP header/CSRCs
495 uint8_t m_connectRetries = 2;
496 uint32_t m_connectRetryDelayMs = 500;
497 uint32_t m_headerTimeoutMs = 4000; // header read timeout
498
499 // Resampling pipeline
500 ResampleStream m_resampler;
501 float m_resampleStep = 1.0f;
502 // Sinks are set directly on the resampler
503
504 // --- RTP/SDP fields ---
505 uint8_t m_payloadType = 0xFF; // unknown by default
506 char m_encoding[32] = {0};
507 AudioInfo m_info{0, 0, 0};
508
509 void resetState() {
510 m_sessionId[0] = '\0';
511 m_serverRtpPort = 0;
512 m_clientRtpPort = 0;
513 m_cseq = 1;
514 m_pktBuf.resize(2048);
515 m_pktBuf.clear();
516 m_decoderReady = false;
517 m_udp_active = false;
518 }
519
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);
523 if (path && *path) {
524 const char* p = path;
525 if (*p == '/') ++p; // skip leading slash
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);
529 // ensure trailing '/'
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';
535 }
536 }
537 }
538 snprintf(m_trackUrl, sizeof(m_trackUrl), "%strackID=0", m_baseUrl);
539 }
540
541 bool openUdpPorts() {
542 // Try a few even RTP ports starting at 5004
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);
546 m_clientRtpPort = p;
547 m_udp_active = true;
548 return true;
549 }
550 }
551 return false;
552 }
553
554 bool fail(const char* msg) {
555 LOGE("RTSPClient: %s", msg);
556 end();
557 return false;
558 }
559
560 void maybeKeepalive() {
561 if (!m_started || !m_tcp.connected()) return;
562 uint32_t now = millis();
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, /*quiet*/ true);
570 } else {
571 sendSimpleRequest("OPTIONS", m_baseUrl, nullptr, 0, m_hdrBuf,
572 sizeof(m_hdrBuf), nullptr, 0, /*quiet*/ true);
573 }
574 }
575
576 // Compute the RTP payload offset inside a UDP packet
577 // Considers fixed RTP header (12 bytes), CSRC count, and configured extra
578 // offset
579 size_t computeRtpPayloadOffset(const uint8_t* data, size_t length) {
580 if (length <= 12) return length;
581 size_t offset = 12;
582 uint8_t cc = data[0] & 0x0F; // CSRC count
583 offset += cc * 4;
584 // Apply any configured additional payload offset (e.g., RFC2250)
585 offset += m_payloadOffset;
586 return offset;
587 }
588
589 void serviceUdp() {
590 // Keep RTSP session alive
591 maybeKeepalive();
592
593 if (!m_udp_active) {
594 LOGE("no UDP");
595 return;
596 }
597 if (m_pktBuf.available() > 0) {
598 LOGI("Still have unprocessed data");
599 return; // still have data buffered
600 }
601
602 // parse next UDP packet
603 int packetSize = m_udp.parsePacket();
604 if (packetSize <= 0) {
605 LOGD("packet size: %d", packetSize);
606 return;
607 }
608
609 // Fill buffer
610 if ((size_t)packetSize > m_pktBuf.size()) m_pktBuf.resize(packetSize);
611 int n = m_udp.read(m_pktBuf.data(), packetSize);
612 m_pktBuf.setAvailable(n);
613 if (n <= 12) {
614 LOGE("packet too small: %d", n);
615 return; // too small to contain RTP
616 }
617
618 // Very basic RTP parsing: compute payload offset
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);
623 }
624
625 // move payload to beginning for contiguous read
626 m_pktBuf.clearArray(payloadOffset);
627 }
628
629 void primeUdpPath() {
630 if (!m_udp_active) return;
631 if (m_serverRtpPort == 0) return;
632 // Send a tiny datagram to server RTP port to open NAT/flows
633 // Not required by RTSP, but improves interoperability
634 for (int i = 0; i < 2; ++i) {
635 m_udp.beginPacket(m_addr, m_serverRtpPort);
636 uint8_t b = 0x00;
637 m_udp.write(&b, 1);
638 m_udp.endPacket();
639 delay(2);
640 }
641 }
642
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) {
649 // restore to be processed by normal path
650 return true;
651 }
652 delay(5);
653 }
654 return false;
655 }
656
657 // Centralized TCP write helper
658 size_t tcpWrite(const uint8_t* data, size_t len) {
659 if (m_tcpCmd.size() < 400) m_tcpCmd.resize(400);
660 return m_tcpCmd.writeArray(data, len);
661 }
662
663 bool tcpCommit() {
664 bool rc = m_tcp.write(m_tcpCmd.data(), m_tcpCmd.available()) ==
665 m_tcpCmd.available();
666 m_tcpCmd.clear();
667 return rc;
668 }
669
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) {
674 // Build request
675 char reqStart[256];
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;
681
682 // Send start line + mandatory headers
683 if (tcpWrite((const uint8_t*)reqStart, reqLen) != (size_t)reqLen) {
684 return false;
685 }
686 // Optional extra headers
687 if (extraHeaders && extraLen) {
688 if (tcpWrite((const uint8_t*)extraHeaders, extraLen) != extraLen) {
689 return false;
690 }
691 }
692 // End of headers
693 const char* end = "\r\n";
694 if (tcpWrite((const uint8_t*)end, 2) != 2) {
695 return false;
696 }
697
698 if (!tcpCommit()) {
699 LOGE("TCP write failed");
700 return false;
701 }
702
703 // Read response headers until CRLFCRLF
704 int hdrUsed = 0;
705 memset(outHeaders, 0, outHeadersLen);
706 if (!readUntilDoubleCRLF(outHeaders, outHeadersLen, hdrUsed,
707 m_headerTimeoutMs)) {
708 if (!quiet) LOGE("RTSPClient: header read timeout");
709 return false;
710 }
711
712 // Optionally read body based on Content-Length
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;
719 outBody[got] = '\0';
720 }
721 return true;
722 }
723
724 bool readUntilDoubleCRLF(char* buf, size_t buflen, int& used,
725 uint32_t timeoutMs = 3000) {
726 uint32_t start = millis();
727 used = 0;
728 int state = 0; // match \r\n\r\n
729 while ((millis() - start) < timeoutMs && used < (int)buflen - 1) {
730 int avail = m_tcp.available();
731 if (avail <= 0) {
732 delay(5);
733 continue;
734 }
735 int n = m_tcp.read((uint8_t*)buf + used, 1);
736 if (n == 1) {
737 char c = buf[used++];
738 switch (state) {
739 case 0:
740 state = (c == '\r') ? 1 : 0;
741 break;
742 case 1:
743 state = (c == '\n') ? 2 : 0;
744 break;
745 case 2:
746 state = (c == '\r') ? 3 : 0;
747 break;
748 case 3:
749 state = (c == '\n') ? 4 : 0;
750 break;
751 }
752 if (state == 4) {
753 buf[used] = '\0';
754 return true;
755 }
756 }
757 }
758 buf[used] = '\0';
759 return false;
760 }
761
762 int readExact(uint8_t* out, int len, uint32_t timeoutMs) {
763 uint32_t start = millis();
764 int got = 0;
765 while (got < len && (millis() - start) < timeoutMs) {
766 int a = m_tcp.available();
767 if (a <= 0) {
768 delay(5);
769 continue;
770 }
771 int n = m_tcp.read(out + got, len - got);
772 if (n > 0) got += n;
773 }
774 return (got == len) ? got : got; // partial OK for DESCRIBE
775 }
776
777 static int parseContentLength(const char* headers) {
778 const char* p = strcasestr(headers, "Content-Length:");
779 if (!p) return 0;
780 int len = 0;
781 if (sscanf(p, "Content-Length: %d", &len) == 1) return len;
782 return 0;
783 }
784
785 void parseSessionFromHeaders(const char* headers) {
786 const char* p = strcasestr(headers, "Session:");
787 if (!p) return;
788 p += 8; // skip "Session:"
789 while (*p == ' ' || *p == '\t') ++p;
790 size_t i = 0;
791 while (*p && *p != '\r' && *p != '\n' && *p != ';' &&
792 i < sizeof(m_sessionId) - 1) {
793 m_sessionId[i++] = *p++;
794 }
795 m_sessionId[i] = '\0';
796 }
797
798 void parseServerPortsFromHeaders(const char* headers) {
799 const char* t = strcasestr(headers, "Transport:");
800 if (!t) return;
801 const char* s = strcasestr(t, "server_port=");
802 if (!s) return;
803 s += strlen("server_port=");
804 int a = 0, b = 0;
805 if (sscanf(s, "%d-%d", &a, &b) == 2) {
806 m_serverRtpPort = (uint16_t)a;
807 }
808 }
809
810 // --- SDP parsing (rtpmap) ---
811 void parseSdp(const char* sdp) {
812 if (!sdp) return;
813 const char* p = sdp;
814 while ((p = strcasestr(p, "a=rtpmap:")) != nullptr) {
815 p += 9; // after a=rtpmap:
816 int pt = 0;
817 if (sscanf(p, "%d", &pt) != 1) continue;
818 const char* space = strchr(p, ' ');
819 if (!space) continue;
820 ++space;
821 // encoding up to '/' or endline
822 size_t i = 0;
823 while (space[i] && space[i] != '/' && space[i] != '\r' &&
824 space[i] != '\n' && i < sizeof(m_encoding) - 1) {
825 m_encoding[i] = space[i];
826 ++i;
827 }
828 m_encoding[i] = '\0';
829 int rate = 0, ch = 0;
830 const char* afterEnc = space + i;
831 if (*afterEnc == '/') {
832 ++afterEnc;
833 if (sscanf(afterEnc, "%d/%d", &rate, &ch) < 1) {
834 rate = 0;
835 ch = 0;
836 }
837 }
838 m_payloadType = (uint8_t)pt;
839 // Fill AudioInfo only for raw PCM encodings
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);
844 } else {
845 m_info = AudioInfo();
846 }
847 m_multi_decoder.setAudioInfo(m_info);
848
849 return; // first match
850 }
851 }
852
853 // --- Content-Base header parsing ---
854 void parseContentBaseFromHeaders(const char* headers) {
855 m_contentBase[0] = '\0';
856 if (!headers) return;
857 const char* p = strcasestr(headers, "Content-Base:");
858 if (!p) return;
859 p += strlen("Content-Base:");
860 while (*p == ' ' || *p == '\t') ++p;
861 size_t i = 0;
862 while (*p && *p != '\r' && *p != '\n' && i < sizeof(m_contentBase) - 1) {
863 m_contentBase[i++] = *p++;
864 }
865 m_contentBase[i] = '\0';
866 // Ensure trailing '/'
867 if (i > 0 && m_contentBase[i - 1] != '/') {
868 if (i + 1 < sizeof(m_contentBase)) {
869 m_contentBase[i++] = '/';
870 m_contentBase[i] = '\0';
871 }
872 }
873 }
874
875 // --- SDP control parsing ---
876 void parseControlFromSdp(const char* sdp) {
877 m_sdpControl[0] = '\0';
878 if (!sdp) return;
879 const char* audio = strcasestr(sdp, "\nm=audio ");
880 const char* searchStart = sdp;
881 const char* searchEnd = nullptr;
882 if (audio) {
883 // find end of this media block (next m= or end)
884 searchStart = audio;
885 const char* nextm = strcasestr(audio + 1, "\nm=");
886 searchEnd = nextm ? nextm : (sdp + strlen(sdp));
887 } else {
888 // fall back to session-level
889 searchStart = sdp;
890 searchEnd = sdp + strlen(sdp);
891 }
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:");
897 // copy value until CR/LF
898 size_t i = 0;
899 while (ctrl[i] && ctrl[i] != '\r' && ctrl[i] != '\n' &&
900 i < sizeof(m_sdpControl) - 1) {
901 m_sdpControl[i] = ctrl[i];
902 ++i;
903 }
904 m_sdpControl[i] = '\0';
905 break;
906 }
907 }
908
909 bool isAbsoluteRtspUrl(const char* url) {
910 if (!url) return false;
911 return (strncasecmp(url, "rtsp://", 7) == 0) ||
912 (strncasecmp(url, "rtsps://", 8) == 0);
913 }
914
915 void buildTrackUrlFromBaseAndControl() {
916 // default fallback if no control provided
917 if (m_sdpControl[0] == '\0') {
918 snprintf(m_trackUrl, sizeof(m_trackUrl), "%strackID=0", m_baseUrl);
919 return;
920 }
921 if (isAbsoluteRtspUrl(m_sdpControl)) {
922 strncpy(m_trackUrl, m_sdpControl, sizeof(m_trackUrl) - 1);
923 m_trackUrl[sizeof(m_trackUrl) - 1] = '\0';
924 return;
925 }
926 const char* base = (m_contentBase[0] ? m_contentBase : m_baseUrl);
927 size_t blen = strlen(base);
928 // Construct base ensuring single '/'
929 char tmp[256];
930 size_t pos = 0;
931 for (; pos < sizeof(tmp) - 1 && pos < blen; ++pos) tmp[pos] = base[pos];
932 if (pos > 0 && tmp[pos - 1] != '/' && pos < sizeof(tmp) - 1)
933 tmp[pos++] = '/';
934 // If control starts with '/', skip one to avoid '//'
935 const char* ctrl = m_sdpControl;
936 if (*ctrl == '/') ++ctrl;
937 while (*ctrl && pos < sizeof(tmp) - 1) tmp[pos++] = *ctrl++;
938 tmp[pos] = '\0';
939 strncpy(m_trackUrl, tmp, sizeof(m_trackUrl) - 1);
940 m_trackUrl[sizeof(m_trackUrl) - 1] = '\0';
941 }
942
943 // resampler is started in constructor; audio info will be set dynamically
944};
945
946} // 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:25
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:47
void setHeaderTimeoutMs(uint32_t ms)
Set timeout (ms) for reading RTSP response headers. Increase if your server responds slowly....
Definition RTSPClient.h:119
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:448
RTSPClient(AudioOutput &out)
Construct with an AudioOutput as decoding sink.
Definition RTSPClient.h:62
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 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
int available()
Returns buffered RTP payload bytes available for copy().
Definition RTSPClient.h:284
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:451
RTSPClient(Print &out)
Construct with a generic Print sink.
Definition RTSPClient.h:70
bool setActive(bool active)
Pause or resume playback via RTSP PAUSE/PLAY.
Definition RTSPClient.h:354
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
void setConnectRetryDelayMs(uint32_t ms)
Set delay between connect retries in ms (default 500ms).
Definition RTSPClient.h:113
void end()
Stop streaming and close RTSP/UDP sockets.
Definition RTSPClient.h:258
uint8_t payloadType() const
RTP payload type from SDP (0xFF if unknown).
Definition RTSPClient.h:347
void setAudioInfo(AudioInfo info) override
Defines the input AudioInfo.
Definition RTSPClient.h:443
void setResampleFactor(float factor)
Set resampling factor to stabilize buffers and playback. 1.0 means no resampling. factor > 1....
Definition RTSPClient.h:92
void setOutput(Print &out)
Define decoding sink as Print.
Definition RTSPClient.h:82
void clearNotifyAudioChange() override
Deletes all change notify subscriptions.
Definition RTSPClient.h:454
void setOutput(AudioStream &out)
Define decoding sink as AudioStream.
Definition RTSPClient.h:78
size_t copy()
Copy the next buffered RTP payload into the decoder pipeline. Performs initial decoder selection base...
Definition RTSPClient.h:392
AudioInfo audioInfo() override
Audio info parsed from SDP for raw PCM encodings.
Definition RTSPClient.h:441
void addDecoder(const char *mimeType, AudioDecoder &decoder)
Register a decoder to be auto-selected for the given MIME.
Definition RTSPClient.h:383
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:296
int available() override
provides the number of entries that are available to read
Definition Buffers.h:233
bool resize(int size)
Resizes the buffer if supported: returns false if not supported.
Definition Buffers.h:305
int writeArray(const T data[], int len) override
Fills the buffer data.
Definition Buffers.h:201
T * data()
Provides address of actual data.
Definition Buffers.h:284
int clearArray(int len) override
consumes len bytes and moves current data to the beginning
Definition Buffers.h:252
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