Files
lgtv_audio_cap/native/src/sinks/sink_http.c
T
Rene KievitsandClaude Opus 5 7529a60650 Audio capture and streaming app for webOS 5/6
Captures the TV's audio and sends it out over several transports. The
primary one is HyperHDR: RTP/L16 to a host-side loopback device, since
HyperHDR has no network audio input of its own. A second route renders
the spectrum on the TV and sends FlatBuffers images to port 19400
instead, for setups where touching the host's sound config is not an
option.

  native/       the service: capture backends (PulseAudio, ALSA, exec,
                test tone, all dlopen-based), DSP, and one file per sink
  frontend/     D-pad driven UI at a fixed 1920x1080
  servicefiles/ native service manifest plus the boot script
  host/         RTP receiver and the loopback installer for the HyperHDR
                machine
  tools/        build/package, asset generation, Homebrew Channel
                manifest, on-TV probe
  test/         host-side suites: FlatBuffers and RTP verified against
                real decoders, the engine end to end, the page in jsdom

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 10:21:00 +02:00

220 lines
6.1 KiB
C

// HTTP audio stream served by the TV.
//
// The friendliest sink to test with, because everything already speaks HTTP:
//
// vlc http://<tv-ip>:4012/audio.wav
// ffplay http://<tv-ip>:4012/audio.wav
// mpv http://<tv-ip>: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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#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://<tv-ip>:%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://<tv-ip>:4012/audio.wav in VLC, ffplay or mpv. Easiest way to confirm capture works.",
.open = http_open,
};