This commit is contained in:
Jannis 2026-09-22 19:23:42 +02:00
commit 21c6884873

244
RTSPMicesp32c6.ino Normal file
View file

@ -0,0 +1,244 @@
#include <WiFi.h>
#include <driver/i2s_std.h>
// --- 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();
}