// HTTP audio stream served by the TV. // // The friendliest sink to test with, because everything already speaks HTTP: // // vlc http://:4012/audio.wav // ffplay http://:4012/audio.wav // mpv http://:4012/audio.wav // // The WAV header declares an unknown length (0xFFFFFFFF sizes), which is the // usual convention for endless streams and what every player above expects. // Request /audio.raw instead to get headerless S16LE, for the odd consumer // that would rather be told the format out of band. #include "sink.h" #include "../common/log.h" #include "../net/streamserv.h" #include #include #include #define WAV_HEADER_BYTES 44 #define WAV_UNKNOWN_SIZE 0xFFFFFFFFu typedef struct { streamserv_t* server; int port; audio_format_t fmt; } http_priv_t; static void put_u32le(uint8_t* p, uint32_t v) { p[0] = (uint8_t)(v & 0xFF); p[1] = (uint8_t)((v >> 8) & 0xFF); p[2] = (uint8_t)((v >> 16) & 0xFF); p[3] = (uint8_t)((v >> 24) & 0xFF); } static void put_u16le(uint8_t* p, uint16_t v) { p[0] = (uint8_t)(v & 0xFF); p[1] = (uint8_t)((v >> 8) & 0xFF); } static void write_wav_header(uint8_t* h, const audio_format_t* fmt) { const uint16_t bits = 16; const uint16_t channels = (uint16_t)fmt->channels; const uint32_t rate = (uint32_t)fmt->rate; const uint16_t block_align = (uint16_t)(channels * (bits / 8)); memcpy(h + 0, "RIFF", 4); put_u32le(h + 4, WAV_UNKNOWN_SIZE); memcpy(h + 8, "WAVE", 4); memcpy(h + 12, "fmt ", 4); put_u32le(h + 16, 16); // PCM fmt chunk length put_u16le(h + 20, 1); // WAVE_FORMAT_PCM put_u16le(h + 22, channels); put_u32le(h + 24, rate); put_u32le(h + 28, rate * block_align); // byte rate put_u16le(h + 32, block_align); put_u16le(h + 34, bits); memcpy(h + 36, "data", 4); put_u32le(h + 40, WAV_UNKNOWN_SIZE); } // Extracts the path from "GET /audio.wav HTTP/1.1". Returns false for anything // that is not a GET, so the server drops the connection. static bool parse_request(const char* request, char* path, size_t pathlen) { if (!request) return false; if (strncmp(request, "GET ", 4) != 0) return false; const char* p = request + 4; while (*p == ' ') p++; size_t n = 0; while (p[n] && p[n] != ' ' && p[n] != '\r' && p[n] != '\n' && n < pathlen - 1) n++; memcpy(path, p, n); path[n] = '\0'; return n > 0; } static bool http_hello(void* user, const char* request, char** out, size_t* out_len) { http_priv_t* p = user; char path[256]; if (!parse_request(request, path, sizeof(path))) { DBG("HTTP sink: rejecting non-GET request"); return false; } // Browsers probe for these; answering them with an audio stream is worse // than refusing outright. if (strcmp(path, "/favicon.ico") == 0 || strcmp(path, "/robots.txt") == 0) return false; bool raw = strstr(path, ".raw") != NULL || strstr(path, ".pcm") != NULL; char headers[512]; int hlen = snprintf(headers, sizeof(headers), "HTTP/1.0 200 OK\r\n" "Content-Type: %s\r\n" "Cache-Control: no-cache, no-store\r\n" "Pragma: no-cache\r\n" "Access-Control-Allow-Origin: *\r\n" "Connection: close\r\n" "\r\n", raw ? "application/octet-stream" : "audio/wav"); if (hlen < 0 || hlen >= (int)sizeof(headers)) return false; size_t total = (size_t)hlen + (raw ? 0 : WAV_HEADER_BYTES); uint8_t* buf = malloc(total); if (!buf) return false; memcpy(buf, headers, (size_t)hlen); if (!raw) write_wav_header(buf + hlen, &p->fmt); INFO("HTTP sink: client requested %s (%s)", path, raw ? "raw S16LE" : "WAV"); *out = (char*)buf; *out_len = total; return true; } static void http_write(sink_t* s, const int16_t* pcm, int frames, const dsp_levels_t* levels) { (void)levels; http_priv_t* p = s->priv; streamserv_broadcast(p->server, pcm, (size_t)frames * (size_t)audio_frame_bytes(&s->fmt)); } static void http_status(sink_t* s, json_writer_t* w) { http_priv_t* p = s->priv; jw_int(w, "port", p->port); jw_int(w, "clients", streamserv_client_count(p->server)); jw_int(w, "droppedBytes", (long long)streamserv_dropped_bytes(p->server)); jw_str(w, "wavPath", "/audio.wav"); jw_str(w, "rawPath", "/audio.raw"); } static void http_close(sink_t* s) { http_priv_t* p = s->priv; if (p) { streamserv_stop(p->server); free(p); } free(s); } static sink_t* http_open(const json_value_t* cfg, const audio_format_t* fmt, char* err, size_t errlen) { const json_value_t* sc = json_get(cfg, "http"); int port = json_int(sc, "port", 4012); if (port <= 0 || port > 65535) { snprintf(err, errlen, "invalid HTTP port %d", port); return NULL; } http_priv_t* p = calloc(1, sizeof(*p)); sink_t* s = calloc(1, sizeof(*s)); if (!p || !s) { free(p); free(s); snprintf(err, errlen, "out of memory"); return NULL; } p->port = port; p->fmt = *fmt; // Players buffer ahead; give them a couple of seconds of slack before we // start dropping. size_t buffer = (size_t)fmt->rate * (size_t)audio_frame_bytes(fmt) * 2; streamserv_config_t scfg = { .port = port, .client_buffer = buffer, .max_clients = json_int(sc, "maxClients", 4), .http_mode = true, .user = p, .hello = http_hello, }; p->server = streamserv_start(&scfg, err, errlen); if (!p->server) { free(p); free(s); return NULL; } s->driver = &sink_driver_http; s->priv = p; s->fmt = *fmt; s->write = http_write; s->status = http_status; s->close = http_close; INFO("HTTP sink: http://:%d/audio.wav (%d Hz, %d ch)", port, fmt->rate, fmt->channels); return s; } const sink_driver_t sink_driver_http = { .id = "http", .name = "HTTP WAV stream", .description = "Open http://:4012/audio.wav in VLC, ffplay or mpv. Easiest way to confirm capture works.", .open = http_open, };