commit 21c68848736f4229041849feccd6075a597a6c3a Author: Jannis Date: Tue Sep 22 19:23:42 2026 +0200 Init diff --git a/RTSPMicesp32c6.ino b/RTSPMicesp32c6.ino new file mode 100644 index 0000000..a33cef2 --- /dev/null +++ b/RTSPMicesp32c6.ino @@ -0,0 +1,244 @@ +#include +#include + +// --- WLAN-Konfiguration --- +const char* ssid = "Stockfeld"; +const char* password = "70691554384740972413"; // Hier dein echtes Passwort eintragen + +// --- Audio- & I2S-Parameter --- +#define SAMPLE_RATE 48000 +#define SAMPLES_PER_PKT 480 // 10 ms Audio pro Frame (verhindert IP-Fragmentierung) + +#define PIN_I2S_BCLK GPIO_NUM_0 // D0 +#define PIN_I2S_WS GPIO_NUM_1 // D1 +#define PIN_I2S_DIN GPIO_NUM_2 // D2 + +// Optionaler digitaler Gain nach korrektem 16-Bit-Downsampling (1 = kein Gain). +// Vorsicht: >1 kann bei lauten Signalen übersteuern, wird aber sauber geclippt. +#define DIGITAL_GAIN 1 + +WiFiServer rtspServer(8554); +WiFiClient rtspClient; +i2s_chan_handle_t rx_chan = NULL; + +uint16_t rtpSeqNum = 0; +uint32_t rtpTimestamp = 0; +const uint32_t rtpSSRC = 0x12345678; +bool isStreaming = false; + +void setupI2S() { + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); + chan_cfg.dma_desc_num = 6; + chan_cfg.dma_frame_num = 240; + + esp_err_t err = i2s_new_channel(&chan_cfg, NULL, &rx_chan); + if (err != ESP_OK) { + Serial.printf("FEHLER: i2s_new_channel() fehlgeschlagen: %s\n", esp_err_to_name(err)); + } + + i2s_std_config_t std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), + // ICS-43434 liefert 24-bit-Samples, MSB-aligned in einem 32-bit-Wort: + .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = { + .mclk = I2S_GPIO_UNUSED, + .bclk = PIN_I2S_BCLK, + .ws = PIN_I2S_WS, + .dout = I2S_GPIO_UNUSED, + .din = PIN_I2S_DIN, + .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false } + } + }; + // WICHTIG: Der L/R-Pin des ICS-43434 muss fest auf GND liegen, damit dieser + // Slot (links) tatsächlich die Mikrofondaten enthält. Liegt L/R auf 3.3V, + // hier auf I2S_STD_SLOT_RIGHT umstellen. + std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT; + + err = i2s_channel_init_std_mode(rx_chan, &std_cfg); + if (err != ESP_OK) { + Serial.printf("FEHLER: i2s_channel_init_std_mode() fehlgeschlagen: %s\n", esp_err_to_name(err)); + } + + err = i2s_channel_enable(rx_chan); + if (err != ESP_OK) { + Serial.printf("FEHLER: i2s_channel_enable() fehlgeschlagen: %s\n", esp_err_to_name(err)); + } +} + +void handleRTSP() { + if (rtspServer.hasClient()) { + WiFiClient newClient = rtspServer.accept(); + if (newClient) { + if (rtspClient) rtspClient.stop(); + rtspClient = newClient; + rtspClient.setTimeout(50); // Timeout auf 50 ms senken, um Blockaden zu verhindern + isStreaming = false; + } + } + + if (!rtspClient || !rtspClient.connected()) return; + + if (rtspClient.available()) { + String req = rtspClient.readStringUntil('\n'); + req.trim(); + if (req.length() == 0) return; + + int cseq = 1; + // Robuster Header-Parser: Wartet bis zum Ende der HTTP/RTSP-Header + while (rtspClient.connected()) { + String line = rtspClient.readStringUntil('\n'); + line.trim(); + if (line.length() == 0) break; + + if (line.startsWith("CSeq:")) { + cseq = line.substring(5).toInt(); + } + } + + if (req.startsWith("OPTIONS")) { + String resp = "RTSP/1.0 200 OK\r\n" + "CSeq: " + String(cseq) + "\r\n" + "Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN, GET_PARAMETER\r\n\r\n"; + rtspClient.print(resp); + } + else if (req.startsWith("DESCRIBE")) { + String ipStr = WiFi.localIP().toString(); + // RFC-konformes SDP mit Verbindungszeile (c=) + String sdp = "v=0\r\n" + "o=- 0 0 IN IP4 " + ipStr + "\r\n" + "s=ESP32-BirdNET-Mic\r\n" + "c=IN IP4 " + ipStr + "\r\n" + "t=0 0\r\n" + "m=audio 0 RTP/AVP 96\r\n" + "a=rtpmap:96 L16/48000/1\r\n" + "a=control:*\r\n"; + + String resp = "RTSP/1.0 200 OK\r\n" + "CSeq: " + String(cseq) + "\r\n" + "Content-Type: application/sdp\r\n" + "Content-Base: rtsp://" + ipStr + ":8554/stream/\r\n" + "Content-Length: " + String(sdp.length()) + "\r\n\r\n" + sdp; + rtspClient.print(resp); + } + else if (req.startsWith("SETUP")) { + // TCP Interleaved zwingend vorgeben (Docker-Bridge kompatibel). + // Hinweis: BirdNET-Go/ffmpeg sollte den Stream mit + // "-rtsp_transport tcp" konsumieren, damit es hier keinen + // Mismatch mit einer eventuell angefragten UDP-Transport gibt. + String resp = "RTSP/1.0 200 OK\r\n" + "CSeq: " + String(cseq) + "\r\n" + "Session: 12345678\r\n" + "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"; + rtspClient.print(resp); + } + else if (req.startsWith("PLAY")) { + String resp = "RTSP/1.0 200 OK\r\n" + "CSeq: " + String(cseq) + "\r\n" + "Session: 12345678\r\n" + "Range: npt=0.000-\r\n\r\n"; + rtspClient.print(resp); + isStreaming = true; + } + else if (req.startsWith("TEARDOWN")) { + rtspClient.print("RTSP/1.0 200 OK\r\nCSeq: " + String(cseq) + "\r\n\r\n"); + isStreaming = false; + } + else if (req.startsWith("GET_PARAMETER")) { + // Wird von ffmpeg/VLC oft als Keepalive während des Streamings + // gesendet. Unbeantwortet würde der Client in ein Timeout laufen + // und ggf. die Verbindung neu aufbauen. + rtspClient.print("RTSP/1.0 200 OK\r\nCSeq: " + String(cseq) + "\r\n\r\n"); + } + else { + // Unbekannte Methode: trotzdem höflich antworten statt zu schweigen. + String resp = "RTSP/1.0 501 Not Implemented\r\n" + "CSeq: " + String(cseq) + "\r\n\r\n"; + rtspClient.print(resp); + } + } +} + +void streamAudio() { + if (!isStreaming || !rtspClient || !rtspClient.connected()) return; + + static int32_t i2s_raw[SAMPLES_PER_PKT]; + size_t bytes_read = 0; + + // DMA liest 480 Samples (10 ms Audio) blockierend mit 20 ms Timeout + esp_err_t res = i2s_channel_read(rx_chan, i2s_raw, sizeof(i2s_raw), &bytes_read, pdMS_TO_TICKS(20)); + if (res != ESP_OK || bytes_read == 0) return; + + int samples = bytes_read / sizeof(int32_t); + static uint8_t packet[4 + 12 + SAMPLES_PER_PKT * 2]; // 4B TCP-Frame + 12B RTP-Header + Audio-Payload + + // Konvertierung von 32-Bit I2S (24-bit-Sample, MSB-aligned) auf 16-Bit Big-Endian (RFC 3550 L16). + // WICHTIG: >>16 extrahiert die oberen 16 Bit des links-bündigen 24-Bit-Samples korrekt. + // Ein optionaler digitaler Gain wird NACH dem Downsampling angewendet und sauber geclippt, + // statt die Rohdaten vor dem int16-Cast zu verstärken (das würde bei Übersteuerung + // zu Wrap-around-Artefakten statt sauberem Clipping führen). + for (int i = 0; i < samples; i++) { + int32_t sample = (i2s_raw[i] >> 16) * DIGITAL_GAIN; + if (sample > 32767) sample = 32767; + if (sample < -32768) sample = -32768; + int16_t s16 = (int16_t)sample; + + packet[16 + i * 2] = (s16 >> 8) & 0xFF; // MSB + packet[16 + i * 2 + 1] = s16 & 0xFF; // LSB + } + + uint16_t rtpSize = 12 + samples * 2; + + // 4-Byte TCP Interleaved Header ($ = 0x24, Channel 0) + packet[0] = '$'; + packet[1] = 0; + packet[2] = (rtpSize >> 8) & 0xFF; + packet[3] = rtpSize & 0xFF; + + // 12-Byte RTP-Header + packet[4] = 0x80; + packet[5] = 96 & 0x7F; // Dynamischer Payload-Typ 96 + packet[6] = (rtpSeqNum >> 8) & 0xFF; + packet[7] = rtpSeqNum & 0xFF; + packet[8] = (rtpTimestamp >> 24) & 0xFF; + packet[9] = (rtpTimestamp >> 16) & 0xFF; + packet[10] = (rtpTimestamp >> 8) & 0xFF; + packet[11] = rtpTimestamp & 0xFF; + packet[12] = (rtpSSRC >> 24) & 0xFF; + packet[13] = (rtpSSRC >> 16) & 0xFF; + packet[14] = (rtpSSRC >> 8) & 0xFF; + packet[15] = rtpSSRC & 0xFF; + + rtpSeqNum++; + rtpTimestamp += samples; + + // In einem Frame absenden, um Fragmentierung zu vermeiden. + // Rückgabewert prüfen: bei vollem TCP-Sendepuffer werden sonst + // stillschweigend Pakete verworfen/unvollständig gesendet. + size_t written = rtspClient.write(packet, 4 + rtpSize); + if (written != (size_t)(4 + rtpSize)) { + Serial.printf("WARNUNG: Nur %u von %u Bytes gesendet (TCP-Puffer voll?)\n", + (unsigned)written, (unsigned)(4 + rtpSize)); + } +} + +void setup() { + Serial.begin(115200); + setupI2S(); + + WiFi.mode(WIFI_STA); + WiFi.setSleep(false); // Verhindert Modem-Sleep und Ping-Latenzspitzen + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(250); + } + + rtspServer.begin(); + Serial.print("RTSP URL: rtsp://"); + Serial.print(WiFi.localIP()); + Serial.println(":8554/stream"); +} + +void loop() { + handleRTSP(); + streamAudio(); +}