diff --git a/src/app_config.c b/src/app_config.c
index 512c588d..2e3e3dbc 100644
--- a/src/app_config.c
+++ b/src/app_config.c
@@ -122,6 +122,7 @@ int app_config_save(void) {
fprintf(file, "rtsp:\n");
fprintf(file, " enable: %s\n", app_config.rtsp_enable ? "true" : "false");
fprintf(file, " port: %d\n", app_config.rtsp_port);
+ fprintf(file, " audio_codec: %s\n", app_config.rtsp_audio_codec);
fprintf(file, " enable_auth: %s\n", app_config.rtsp_enable_auth ? "true" : "false");
fprintf(file, " auth_user: %s\n", app_config.rtsp_auth_user);
fprintf(file, " auth_pass: %s\n", app_config.rtsp_auth_pass);
@@ -236,6 +237,7 @@ enum ConfigError app_config_parse(void) {
app_config.rtsp_enable = false;
app_config.rtsp_port = 554;
+ strcpy(app_config.rtsp_audio_codec, "mp3"); /* upstream default; the Fullhan image ships pcma in its yaml */
app_config.rtsp_enable_auth = false;
app_config.rtsp_auth_user[0] = '\0';
app_config.rtsp_auth_pass[0] = '\0';
@@ -432,6 +434,18 @@ enum ConfigError app_config_parse(void) {
parse_bool(&ini, "rtsp", "enable", &app_config.rtsp_enable);
parse_int(&ini, "rtsp", "port", 0, USHRT_MAX, &app_config.rtsp_port);
+ {
+ /* Bounded, and only the two codecs the RTP path implements: anything
+ * else would advertise a payload type nothing produces. */
+ char codec[16] = {0};
+ if (parse_param_value_n(&ini, "rtsp", "audio_codec", codec, sizeof(codec)) == CONFIG_OK) {
+ if (EQUALS(codec, "pcma") || EQUALS(codec, "mp3")) {
+ strncpy(app_config.rtsp_audio_codec, codec,
+ sizeof(app_config.rtsp_audio_codec) - 1);
+ app_config.rtsp_audio_codec[sizeof(app_config.rtsp_audio_codec) - 1] = 0;
+ }
+ }
+ }
if (app_config.rtsp_enable) {
parse_bool(&ini, "rtsp", "enable_auth", &app_config.rtsp_enable_auth);
parse_param_value(
diff --git a/src/app_config.h b/src/app_config.h
index 1bf42c5f..251b9995 100644
--- a/src/app_config.h
+++ b/src/app_config.h
@@ -60,6 +60,7 @@ struct AppConfig {
char rtsp_auth_user[32];
char rtsp_auth_pass[32];
int rtsp_port;
+ char rtsp_audio_codec[8]; /* pcma (G.711 A-law, 8 kHz) or mp3 */
// [record]
bool record_enable;
diff --git a/src/fmt/bitbuf.c b/src/fmt/bitbuf.c
index b3c76ec0..d5a228a6 100644
--- a/src/fmt/bitbuf.c
+++ b/src/fmt/bitbuf.c
@@ -1,6 +1,7 @@
#include
#include
#include
+#include
#include "bitbuf.h"
@@ -57,8 +58,7 @@ enum BufError put_to_offset(
const uint32_t size) {
chk_ptr uint32_t pos = offset + size;
if (pos >= ptr->size)
- chk_realloc for (uint32_t i = 0; i < size; i++) ptr->buf[offset + i] =
- data[i];
+ chk_realloc memcpy(ptr->buf + offset, data, size);
return BUF_OK;
}
enum BufError put(struct BitBuf *ptr, const char *data, const uint32_t size) {
diff --git a/src/fmt/moov.c b/src/fmt/moov.c
index 9cbe9a20..350b38cc 100644
--- a/src/fmt/moov.c
+++ b/src/fmt/moov.c
@@ -607,7 +607,8 @@ enum BufError write_DecoderConfig(struct BitBuf *ptr, const struct MoovInfo *moo
err = put_u32_be(ptr, 0);
chk_err;
- err = put_u8(ptr, moov_info->audio_codec);
+ err = put_u8(ptr, (moov_info->audio_codec == ACODEC_ID_MP3 && moov_info->audio_samplerate >= 32000) ?
+ 0x6B /* MPEG-1 audio */ : moov_info->audio_codec);
chk_err; // objectTypeIndication, https://mp4ra.org/#/object_types
err = put_u8(ptr, 0x15);
chk_err; // streamType
diff --git a/src/fmt/mp4.c b/src/fmt/mp4.c
index 92e2b932..24d22895 100644
--- a/src/fmt/mp4.c
+++ b/src/fmt/mp4.c
@@ -60,6 +60,10 @@ enum BufError create_header(char is_h265) {
void mp4_set_config(short width, short height, char framerate, char acodec,
unsigned short bitrate, char channels, unsigned int srate) {
+ /* A new stream configuration (codec, size) invalidates the cached header
+ * and parameter sets; without this a codec switch keeps sending the old one */
+ buf_header.offset = 0;
+ buf_sps_len = buf_pps_len = buf_vps_len = 0;
vid_width = width;
vid_height = height;
vid_framerate = framerate;
diff --git a/src/hal/config.c b/src/hal/config.c
index b5279cfc..f5f13e14 100644
--- a/src/hal/config.c
+++ b/src/hal/config.c
@@ -58,6 +58,12 @@ enum ConfigError section_pos(
enum ConfigError parse_param_value(
struct IniConfig *ini, const char *section, const char *param_name,
char *param_value) {
+ return parse_param_value_n(ini, section, param_name, param_value, (size_t)-1);
+}
+
+enum ConfigError parse_param_value_n(
+ struct IniConfig *ini, const char *section, const char *param_name,
+ char *param_value, size_t param_value_size) {
int start_pos = 0;
int end_pos = 0;
if (strlen(section) > 0) {
@@ -82,8 +88,19 @@ enum ConfigError parse_param_value(
if (match > 0 || (end_pos >= 0 && end_pos - start_pos < m[1].rm_so))
return CONFIG_PARAM_NOT_FOUND;
- int res = sprintf(param_value, "%.*s", (int)(m[1].rm_eo - m[1].rm_so),
- ini->str + start_pos + m[1].rm_so);
+ int len = (int)(m[1].rm_eo - m[1].rm_so);
+ int res;
+ if (param_value_size != (size_t)-1) {
+ if ((size_t)len >= param_value_size)
+ len = (int)param_value_size - 1;
+ res = snprintf(param_value, param_value_size, "%.*s", len,
+ ini->str + start_pos + m[1].rm_so);
+ if (res >= (int)param_value_size)
+ res = (int)param_value_size - 1;
+ } else {
+ res = sprintf(param_value, "%.*s", len,
+ ini->str + start_pos + m[1].rm_so);
+ }
param_value[res] = 0;
if (res >= 2 && param_value[0] == '"' && param_value[res - 1] == '"') {
diff --git a/src/hal/config.h b/src/hal/config.h
index 0045ee40..a15e732a 100644
--- a/src/hal/config.h
+++ b/src/hal/config.h
@@ -41,6 +41,12 @@ bool open_config(struct IniConfig *ini, FILE **file);
enum ConfigError find_sections(struct IniConfig *ini);
enum ConfigError section_pos(
struct IniConfig *ini, const char *section, int *start_pos, int *end_pos);
+/* Bounded form of parse_param_value(): the unbounded one sprintf()s the matched
+ * value straight into the caller's buffer, which overflows small fields. */
+enum ConfigError parse_param_value_n(
+ struct IniConfig *ini, const char *section, const char *param_name,
+ char *param_value, size_t param_value_size);
+
enum ConfigError parse_param_value(
struct IniConfig *ini, const char *section, const char *param_name,
char *param_value);
diff --git a/src/main.c b/src/main.c
index f660a169..b6940bb2 100644
--- a/src/main.c
+++ b/src/main.c
@@ -73,6 +73,7 @@ int main(int argc, char *argv[]) {
if (app_config.rtsp_enable) {
rtspHandle = rtsp_create(RTSP_MAXIMUM_CONNECTIONS, app_config.rtsp_port, 1);
+ rtsp_latch_audio_codec();
HAL_INFO("rtsp", "Started listening for clients...\n");
if (app_config.rtsp_enable_auth) {
if (EMPTY(app_config.rtsp_auth_user) || EMPTY(app_config.rtsp_auth_pass))
diff --git a/src/media.c b/src/media.c
index f3d6f58f..c142120f 100644
--- a/src/media.c
+++ b/src/media.c
@@ -39,6 +39,67 @@ int save_audio_stream(hal_audframe *frame) {
return EXIT_SUCCESS;
}
+static unsigned char pcm_to_alaw(short pcm)
+{
+ int sign = (pcm & 0x8000) >> 8, s = sign ? -pcm - 1 : pcm, exp = 7;
+ if (s > 32635) s = 32635;
+ if (s >= 256) {
+ for (int m = 0x4000; !(s & m) && exp > 0; m >>= 1) exp--;
+ s = ((exp << 4) | ((s >> (exp + 3)) & 0x0f));
+ } else
+ s >>= 4;
+ return (unsigned char)((s ^ (sign ? 0xd5 : 0x55)));
+}
+
+/* RTSP G.711: resample the capture rate to 8 kHz (block average) and A-law
+ * encode, sending one RTP packet per 20 ms; MP3 keeps feeding the MP4/HTTP/RTMP
+ * outputs.
+ *
+ * The ratio is carried in 16.16 fixed point rather than as an integer divisor:
+ * srate is accepted anywhere in 8000..96000, and truncating 44100 / 8000 to 5
+ * emitted 8820 samples a second against an SDP and RTP clock of 8000, so the
+ * stream ran fast and the 160-byte packets were 18.1 ms rather than 20. */
+static void rtsp_pcma_feed(short *pcm, unsigned int samples)
+{
+ static unsigned char alaw[160];
+ static unsigned int fill, phase, step;
+ static int acc, accN;
+ if (!step) {
+ unsigned int srate = app_config.audio_srate > 0 ? app_config.audio_srate : 8000;
+ step = (srate << 16) / 8000;
+ if (!step) step = 1 << 16;
+ }
+ for (unsigned int i = 0; i < samples; i++) {
+ acc += pcm[i];
+ accN++;
+ phase += 1 << 16;
+ if (phase < step) continue;
+ phase -= step;
+ alaw[fill++] = pcm_to_alaw((short)(acc / accN));
+ acc = 0; accN = 0;
+ if (fill == sizeof(alaw)) {
+ rtp_send_pcma(rtspHandle, alaw, sizeof(alaw));
+ fill = 0;
+ }
+ }
+}
+
+/* The codec RTSP sessions were negotiated with.
+ *
+ * app_config.rtsp_audio_codec can be changed at runtime through /api/rtsp, but
+ * switching the payload type and clock under a live session would leave every
+ * client decoding the wrong thing without a new DESCRIBE/SETUP. The value the
+ * media path uses is therefore latched when the server starts, which is what
+ * the API means when it says the change applies after a restart. */
+static char rtspCodec[sizeof(app_config.rtsp_audio_codec)];
+
+void rtsp_latch_audio_codec(void) {
+ strncpy(rtspCodec, app_config.rtsp_audio_codec, sizeof(rtspCodec) - 1);
+ rtspCodec[sizeof(rtspCodec) - 1] = 0;
+}
+
+static char rtsp_pcma_active(void) { return EQUALS(rtspCodec, "pcma"); }
+
void *aenc_thread(void) {
static uint8_t frame_buf[AUD_FRAME_MAX + 2];
const uint32_t mp3FrmSize =
@@ -56,7 +117,7 @@ void *aenc_thread(void) {
pthread_mutex_lock(&mp4Mtx);
mp4_ingest_audio(mp3Buf.buf, mp3FrmSize);
pthread_mutex_unlock(&mp4Mtx);
- if (app_config.rtsp_enable)
+ if (app_config.rtsp_enable && !rtsp_pcma_active())
rtp_send_mp3(rtspHandle, mp3Buf.buf, mp3FrmSize);
rtmp_ingest_audio(mp3Buf.buf, mp3FrmSize);
mp3Buf.offset -= mp3FrmSize;
@@ -83,6 +144,18 @@ void *aenc_thread(void) {
outFrame.seq = seq++;
outFrame.timestamp = millis();
send_pcm_to_client(&outFrame);
+ if (app_config.rtsp_enable && rtsp_pcma_active())
+ rtsp_pcma_feed((short *)(frame_buf + 2), flen / 2);
+
+ /* The software MP3 encoder is the single most expensive thing on a
+ * small SoC (24% of an ARM1176 at 48 kHz); skip it while nothing
+ * consumes MP3: no HTTP MP3/MP4 client, no recording, no RTMP, and
+ * RTSP carrying G.711 */
+ if (!recordOn && !app_config.stream_enable && !http_audio_clients() &&
+ !(app_config.rtsp_enable && !rtsp_pcma_active())) {
+ pcmPos = 0;
+ continue;
+ }
unsigned int pcmLen = flen / 2;
short *pcmPack = (short *)(frame_buf + 2);
diff --git a/src/media.h b/src/media.h
index 5adb8d5c..f7172e4b 100644
--- a/src/media.h
+++ b/src/media.h
@@ -47,3 +47,5 @@ int media_mjpeg_disable(void);
int media_mjpeg_enable(void);
int media_mp4_disable(void);
int media_mp4_enable(void);
+
+void rtsp_latch_audio_codec(void);
diff --git a/src/rtsp/rtp.c b/src/rtsp/rtp.c
index 1bb504a7..71e02933 100644
--- a/src/rtsp/rtp.c
+++ b/src/rtsp/rtp.c
@@ -5,6 +5,7 @@
#include
#include
#include
+#include
#include "rtsp_server.h"
#include "common.h"
@@ -35,6 +36,46 @@ struct __transfer_set_t {
int track_id;
};
+/* 90 kHz video / 8 kHz G.711 timestamps of the frame being sent */
+static unsigned int __frame_ts_video, __frame_ts_audio;
+
+/* Block until the socket can take more data (or 100 ms), instead of spinning */
+static inline void __wait_out(int fd)
+{
+ struct pollfd p = { .fd = fd, .events = POLLOUT };
+ poll(&p, 1, 100);
+}
+
+/* Write out the staged interleaved data; call with write_mutex held */
+static int __tcp_flush(struct connection_item_t *con)
+{
+ unsigned int sent = 0;
+ while (sent < con->tx_len) {
+ int r = send(con->client_fd, con->tx_buf + sent, con->tx_len - sent, 0);
+ if (r > 0) sent += r;
+ else if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { __wait_out(con->client_fd); }
+ else { con->tx_len = 0; return FAILURE; }
+ }
+ con->tx_len = 0;
+ return SUCCESS;
+}
+
+static int __tcp_flush_each(struct list_t *e, void *v)
+{
+ struct transfer_item_t *trans;
+ struct connection_item_t *con;
+ list_upcast(trans, e);
+ MUST(con = trans->con, return FAILURE);
+ /* Packets are only ever staged from the interleaved branch below, so
+ * anything in tx_buf came from a TCP transfer whichever track sent it.
+ * Testing track 0 here never flushed a client that set up audio alone. */
+ if (!con->tx_buf || !con->tx_len) return SUCCESS;
+ pthread_mutex_lock(&con->write_mutex);
+ int ret = __tcp_flush(con);
+ pthread_mutex_unlock(&con->write_mutex);
+ return ret;
+}
+
/******************************************************************************
* PRIVATE FUNCTIONS
******************************************************************************/
@@ -152,6 +193,26 @@ static inline int __transfer_nal_mpga(struct list_head_t *trans_list, unsigned c
return SUCCESS;
}
+/* One RTP packet per chunk of G.711 A-law bytes (20 ms = 160 bytes at 8 kHz) */
+static inline int __transfer_pcma(struct list_head_t *trans_list, unsigned char *ptr, size_t size)
+{
+ struct nal_rtp_t rtp;
+ rtp_hdr_t *p_header = &(rtp.packet.header);
+
+ p_header->version = 2;
+ p_header->p = 0;
+ p_header->x = 0;
+ p_header->cc = 0;
+ p_header->pt = 8;
+ p_header->m = 1;
+
+ memcpy(rtp.packet.payload, ptr, size);
+ rtp.rtpsize = size + sizeof(rtp_hdr_t);
+
+ ASSERT(__rtp_send(&rtp, trans_list) == SUCCESS, return FAILURE);
+ return SUCCESS;
+}
+
static inline int __rtp_send_eachconnection(struct list_t *e, void *v)
{
int send_bytes;
@@ -166,8 +227,11 @@ static inline int __rtp_send_eachconnection(struct list_t *e, void *v)
if (!con->trans[track_id].server_port_rtp && !con->trans[track_id].is_tcp) return SUCCESS;
rtp->packet.header.seq = htons(con->trans[track_id].rtp_seq);
- if (rtp->packet.header.m)
- con->trans[track_id].rtp_timestamp = (millis() * 90) & UINT32_MAX;
+ /* One timestamp per frame, taken when the frame starts (see rtp_send_*):
+ * stamping only the marker packet gave every earlier packet of a frame
+ * the previous frame's time, so receivers saw timestamps run backwards
+ * inside a frame and players stalled and then raced to catch up */
+ con->trans[track_id].rtp_timestamp = track_id ? __frame_ts_audio : __frame_ts_video;
rtp->packet.header.ts = htonl(con->trans[track_id].rtp_timestamp);
rtp->packet.header.ssrc = htonl(con->ssrc);
con->trans[track_id].rtp_seq += 1;
@@ -180,11 +244,30 @@ static inline int __rtp_send_eachconnection(struct list_t *e, void *v)
head[3] = rtp->rtpsize & 0xFF;
pthread_mutex_lock(&con->write_mutex);
+ if (con->tx_buf) {
+ /* stage the packet; one send() per frame or per RTSP_TX_BATCH */
+ int ok = 1;
+ if (con->tx_len + 4 + rtp->rtpsize > RTSP_TX_BATCH)
+ ok = __tcp_flush(con) == SUCCESS;
+ if (ok) {
+ memcpy(con->tx_buf + con->tx_len, head, 4);
+ memcpy(con->tx_buf + con->tx_len + 4, &(rtp->packet), rtp->rtpsize);
+ con->tx_len += 4 + rtp->rtpsize;
+ }
+ pthread_mutex_unlock(&con->write_mutex);
+ if (ok) {
+ con->trans[track_id].rtcp_packet_cnt += 1;
+ con->trans[track_id].rtcp_octet += rtp->rtpsize;
+ return SUCCESS;
+ }
+ ERR("send (staged):%s\n", strerror(errno));
+ return FAILURE;
+ }
int sent_h = 0;
while (sent_h < 4) {
int r = send(con->client_fd, head + sent_h, 4 - sent_h, 0);
if (r > 0) sent_h += r;
- else if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) usleep(1000);
+ else if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { __wait_out(con->client_fd); }
else { sent_h = -1; break; }
}
if (sent_h == 4) {
@@ -192,7 +275,7 @@ static inline int __rtp_send_eachconnection(struct list_t *e, void *v)
while (sent_b < rtp->rtpsize) {
int r = send(con->client_fd, (char*)&(rtp->packet) + sent_b, rtp->rtpsize - sent_b, 0);
if (r > 0) sent_b += r;
- else if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) usleep(1000);
+ else if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { __wait_out(con->client_fd); }
else { sent_b = -1; break; }
}
send_bytes = sent_b;
@@ -423,6 +506,12 @@ int rtp_send_h26x(rtsp_handle h, hal_vidstream *stream, char isH265)
}
h->isH265 = isH265;
+ /* Stamp with the encoder's capture time (pack timestamps are microseconds)
+ * so receivers pace frames correctly even when a large keyframe makes the
+ * sender deliver the following frames in a burst; fall back to the send
+ * time for HALs that leave the timestamp at zero */
+ __frame_ts_video = (stream->count && stream->pack[0].timestamp) ?
+ (unsigned int)(stream->pack[0].timestamp * 9 / 100) : ((millis() * 90) & UINT32_MAX);
for (int i = 0; i < stream->count; i++) {
ASSERT(__retrieve_sprop(h, stream->pack[i].data + stream->pack[i].offset,
@@ -436,7 +525,7 @@ int rtp_send_h26x(rtsp_handle h, hal_vidstream *stream, char isH265)
rtsp_lock(h);
ASSERT(list_map_inline(&h->con_list, (__rtp_setup_transfer), &trans) == SUCCESS, ({rtsp_unlock(h); goto error;}));
rtsp_unlock(h);
-
+
if (trans.list_head.list) {
for (int i = 0; i < stream->count; i++) {
unsigned char *data = stream->pack[i].data + stream->pack[i].offset;
@@ -451,6 +540,7 @@ int rtp_send_h26x(rtsp_handle h, hal_vidstream *stream, char isH265)
ASSERT(__transfer_nal_h26x(&(trans.list_head), data, length, h->isH265) == SUCCESS, goto error);
}
}
+ ASSERT(list_map_inline(&(trans.list_head), (__tcp_flush_each), NULL) == SUCCESS, goto error);
ASSERT(list_map_inline(&(trans.list_head), (__rtcp_poll), &track_id) == SUCCESS, goto error);
}
@@ -462,8 +552,46 @@ int rtp_send_h26x(rtsp_handle h, hal_vidstream *stream, char isH265)
return ret;
}
+int rtp_send_pcma(rtsp_handle h, unsigned char *buf, size_t len)
+{
+ /* G.711 is one byte per sample, so the 8 kHz clock advances by exactly the
+ * number of samples in the packet. Deriving this from millis() instead let
+ * the stamps drift away from the audio actually sent. */
+ static unsigned int pcma_ts;
+ __frame_ts_audio = pcma_ts;
+ pcma_ts += (unsigned int)len;
+ int ret = FAILURE;
+ int track_id = 1;
+ struct __transfer_set_t trans = {};
+
+ DASSERT(h, return FAILURE);
+ if (gbl_get_quit(h->pool->sharedp->gbl))
+ return FAILURE;
+
+ h->audioPt = 8;
+
+ trans.h = h;
+ trans.track_id = track_id;
+
+ rtsp_lock(h);
+ ASSERT(list_map_inline(&h->con_list, (__rtp_setup_transfer), &trans) == SUCCESS, ({rtsp_unlock(h); goto error;}));
+ rtsp_unlock(h);
+
+ if (trans.list_head.list) {
+ ASSERT(__transfer_pcma(&(trans.list_head), buf, len) == SUCCESS, goto error);
+ ASSERT(list_map_inline(&(trans.list_head), (__tcp_flush_each), NULL) == SUCCESS, goto error);
+ ASSERT(list_map_inline(&(trans.list_head), (__rtcp_poll), &track_id) == SUCCESS, goto error);
+ }
+ ret = SUCCESS;
+
+error:
+ list_destroy(&(trans.list_head));
+ return ret;
+}
+
int rtp_send_mp3(rtsp_handle h, unsigned char *buf, size_t len)
{
+ __frame_ts_audio = (millis() * 90) & UINT32_MAX; /* 90 kHz MPEG audio */
int ret = FAILURE;
int track_id = 1;
struct __transfer_set_t trans = {};
@@ -490,6 +618,7 @@ int rtp_send_mp3(rtsp_handle h, unsigned char *buf, size_t len)
if (trans.list_head.list) {
ASSERT(__transfer_nal_mpga(&(trans.list_head), buf, len) == SUCCESS, goto error);
+ ASSERT(list_map_inline(&(trans.list_head), (__tcp_flush_each), NULL) == SUCCESS, goto error);
ASSERT(list_map_inline(&(trans.list_head), (__rtcp_poll), &track_id) == SUCCESS, goto error);
}
diff --git a/src/rtsp/rtsp.c b/src/rtsp/rtsp.c
index 7219c862..4a61d68d 100644
--- a/src/rtsp/rtsp.c
+++ b/src/rtsp/rtsp.c
@@ -191,8 +191,8 @@ static void __method_describe(struct connection_item_t *p, rtsp_handle h)
"\r\n"
"m=audio 0 RTP/AVP %d\r\n"
"a=control:track=1\r\n"
- "a=rtpmap:%d %s/90000\r\n",
- h->audioPt, h->audioPt, audioRtpfmt);
+ "a=rtpmap:%d %s/%d\r\n",
+ h->audioPt, h->audioPt, audioRtpfmt, (h->audioPt == 8 || h->audioPt == 0) ? 8000 : 90000);
}
if (h->isH265 &&
@@ -563,6 +563,8 @@ __connection_list_add(bufpool_handle con_pool, struct list_head_t *head, int fd,
ASSERT((p->fp_tcp_read = fdopen(fd, "r")), goto error);
ASSERT((p->fp_tcp_write = fdopen(fd, "w")), goto error);
+ p->tx_len = 0;
+ if (!p->tx_buf) p->tx_buf = malloc(RTSP_TX_BATCH);
p->con_state = __CON_S_INIT;
return list_add(head, &(p->list_entry));
@@ -655,6 +657,11 @@ static inline int __bind_rtp(struct connection_item_t *con )
tmp = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &tmp, sizeof(tmp));
+ {
+ int sz = 512 * 1024; /* absorb a whole keyframe, see the TCP accept path */
+ if (setsockopt(server_fd, SOL_SOCKET, SO_SNDBUFFORCE, &sz, sizeof(sz)))
+ setsockopt(server_fd, SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
+ }
ASSERT(bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == 0, ({
ERR("bind:%s\n", strerror(errno));
@@ -748,6 +755,17 @@ static inline int __accept_proc_sock(rtsp_handle h, int server_fd, struct sock_s
/* set server fd to non-blocking */
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
+ /* A keyframe is a few hundred KB and the kernel's default TCP send
+ * buffer (64 KB on small boards) cannot hold it: the interleaved sender
+ * then spins on partial writes for as long as the link takes to drain,
+ * hundreds of ms of CPU per keyframe. Ask for a buffer that fits one
+ * (SO_SNDBUFFORCE bypasses wmem_max, which needs root). */
+ {
+ int sz = 512 * 1024;
+ if (setsockopt(fd, SOL_SOCKET, SO_SNDBUFFORCE, &sz, sizeof(sz)))
+ setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
+ }
+
/* update connection-list exclusively */
ASSERT(__connection_list_add(h->con_pool, &h->con_list, fd, from_addr) == SUCCESS,
return FAILURE);
diff --git a/src/rtsp/rtsp.h b/src/rtsp/rtsp.h
index 18c7333a..dbd2e120 100644
--- a/src/rtsp/rtsp.h
+++ b/src/rtsp/rtsp.h
@@ -113,8 +113,13 @@ struct connection_item_t {
unsigned long long given_session_id;
unsigned int ssrc;
pthread_mutex_t write_mutex;
+ /* Interleaved (TCP) output is staged here and written with one send()
+ * per frame: on small SoCs a send() costs ~100 us whatever its size */
+ unsigned char *tx_buf;
+ unsigned int tx_len;
struct list_t list_entry;
};
+#define RTSP_TX_BATCH (64 * 1024)
struct transfer_item_t {
struct list_t list_entry;
diff --git a/src/rtsp/rtsp_server.h b/src/rtsp/rtsp_server.h
index f50538ac..522aa67c 100644
--- a/src/rtsp/rtsp_server.h
+++ b/src/rtsp/rtsp_server.h
@@ -32,6 +32,7 @@ typedef struct __rtsp_obj_t *rtsp_handle;
void rtp_disable_audio(rtsp_handle h);
int rtp_send_h26x(rtsp_handle h, hal_vidstream *stream, char isH265);
int rtp_send_mp3(rtsp_handle h, unsigned char *buf, size_t len);
+int rtp_send_pcma(rtsp_handle h, unsigned char *buf, size_t len);
extern void rtsp_finish(rtsp_handle h);
diff --git a/src/server.c b/src/server.c
index 16340140..400764b2 100644
--- a/src/server.c
+++ b/src/server.c
@@ -218,7 +218,41 @@ void send_h26x_to_client(char index, hal_vidstream *stream) {
}
}
+/* Escape a string for use inside a JSON string literal. Usernames and codec
+ * names come from a request or the config file, and an unescaped quote or
+ * backslash makes the response unparseable for the web UI's JSON.parse(). */
+static const char *json_str(char *dst, size_t dstsz, const char *src) {
+ size_t o = 0;
+ for (; src && *src && o + 7 < dstsz; src++) {
+ unsigned char c = (unsigned char)*src;
+ if (c == '"' || c == '\\') { dst[o++] = '\\'; dst[o++] = c; }
+ else if (c < 0x20) o += snprintf(dst + o, dstsz - o, "\\u%04x", c);
+ else dst[o++] = c;
+ }
+ dst[o] = 0;
+ return dst;
+}
+
+/* Whether any HTTP client is consuming the MP3 encoder's output (raw MP3 or MP4) */
+char http_audio_clients(void) {
+ char any = 0;
+ pthread_mutex_lock(&client_fds_mutex);
+ for (unsigned int i = 0; i < HTTP_MAX_CLIENTS; ++i)
+ if (client_fds[i].sockFd >= 0 &&
+ (client_fds[i].type == STREAM_MP4 || client_fds[i].type == STREAM_MP3)) { any = 1; break; }
+ pthread_mutex_unlock(&client_fds_mutex);
+ return any;
+}
+
void send_mp4_to_client(char index, hal_vidstream *stream, char isH265) {
+ /* Building moof/mdat for every frame is wasted work without a consumer;
+ * the parameter sets are still cached so a header is ready when one connects */
+ char anyClient = 0;
+ pthread_mutex_lock(&client_fds_mutex);
+ for (unsigned int i = 0; i < HTTP_MAX_CLIENTS; ++i)
+ if (client_fds[i].sockFd >= 0 && client_fds[i].type == STREAM_MP4) { anyClient = 1; break; }
+ pthread_mutex_unlock(&client_fds_mutex);
+
for (unsigned int i = 0; i < stream->count; ++i) {
hal_vidpack *pack = &stream->pack[i];
unsigned char *pack_data = pack->data + pack->offset;
@@ -235,11 +269,14 @@ void send_mp4_to_client(char index, hal_vidstream *stream, char isH265) {
mp4_set_pps(pack_data + pack->nalu[j].offset + scLen, pack->nalu[j].length - scLen, isH265);
else if (pack->nalu[j].type == NalUnitType_VPS_HEVC && pack->nalu[j].length <= UINT16_MAX)
mp4_set_vps(pack_data + pack->nalu[j].offset + scLen, pack->nalu[j].length - scLen);
+ else if (!anyClient)
+ continue;
else if (pack->nalu[j].type == NalUnitType_CodedSliceIdr || pack->nalu[j].type == NalUnitType_CodedSliceAux)
mp4_set_slice(pack_data + pack->nalu[j].offset + scLen, pack->nalu[j].length - scLen, 1);
else if (pack->nalu[j].type == NalUnitType_CodedSliceNonIdr)
mp4_set_slice(pack_data + pack->nalu[j].offset + scLen, pack->nalu[j].length - scLen, 0);
}
+ if (!anyClient) continue;
static enum BufError err;
char len_buf[50];
@@ -1177,6 +1214,83 @@ void respond_request(http_request_t *req) {
return;
}
+ if (EQUALS(req->uri, "/api/rtsp")) {
+ if (req->query) {
+ char *remain;
+ while (req->query) {
+ char *value = split(&req->query, "&");
+ if (!value || !*value) continue;
+ unescape_uri(value);
+ char *key = split(&value, "=");
+ if (!key || !*key || !value || !*value) continue;
+ if (EQUALS(key, "enable"))
+ app_config.rtsp_enable = EQUALS_CASE(value, "true") || EQUALS(value, "1");
+ else if (EQUALS(key, "enable_auth"))
+ app_config.rtsp_enable_auth = EQUALS_CASE(value, "true") || EQUALS(value, "1");
+ else if (EQUALS(key, "port")) {
+ int result = strtol(value, &remain, 10);
+ if (remain != value && result > 0 && result < 65536)
+ app_config.rtsp_port = result;
+ } else if (EQUALS(key, "auth_user"))
+ strncpy(app_config.rtsp_auth_user, value, sizeof(app_config.rtsp_auth_user) - 1);
+ else if (EQUALS(key, "auth_pass"))
+ strncpy(app_config.rtsp_auth_pass, value, sizeof(app_config.rtsp_auth_pass) - 1);
+ else if (EQUALS(key, "audio_codec")) {
+ /* only the codecs the RTP path implements */
+ if (EQUALS(value, "pcma") || EQUALS(value, "mp3")) {
+ strncpy(app_config.rtsp_audio_codec, value,
+ sizeof(app_config.rtsp_audio_codec) - 1);
+ app_config.rtsp_audio_codec[sizeof(app_config.rtsp_audio_codec) - 1] = 0;
+ }
+ }
+ }
+ }
+ char escUser[sizeof(app_config.rtsp_auth_user) * 6 + 1];
+ char escCodec[sizeof(app_config.rtsp_audio_codec) * 6 + 1];
+ respLen = sprintf(response,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: application/json;charset=UTF-8\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "{\"enable\":%s,\"enable_auth\":%s,\"port\":%d,\"auth_user\":\"%s\",\"audio_codec\":\"%s\","
+ "\"note\":\"port and codec changes apply after restart\"}",
+ app_config.rtsp_enable ? "true" : "false", app_config.rtsp_enable_auth ? "true" : "false",
+ app_config.rtsp_port,
+ json_str(escUser, sizeof(escUser), app_config.rtsp_auth_user),
+ json_str(escCodec, sizeof(escCodec), app_config.rtsp_audio_codec));
+ send_and_close(req->clntFd, response, respLen);
+ return;
+ }
+ if (EQUALS(req->uri, "/api/onvif")) {
+ if (req->query) {
+ while (req->query) {
+ char *value = split(&req->query, "&");
+ if (!value || !*value) continue;
+ unescape_uri(value);
+ char *key = split(&value, "=");
+ if (!key || !*key || !value || !*value) continue;
+ if (EQUALS(key, "enable"))
+ app_config.onvif_enable = EQUALS_CASE(value, "true") || EQUALS(value, "1");
+ else if (EQUALS(key, "enable_auth"))
+ app_config.onvif_enable_auth = EQUALS_CASE(value, "true") || EQUALS(value, "1");
+ else if (EQUALS(key, "auth_user"))
+ strncpy(app_config.onvif_auth_user, value, sizeof(app_config.onvif_auth_user) - 1);
+ else if (EQUALS(key, "auth_pass"))
+ strncpy(app_config.onvif_auth_pass, value, sizeof(app_config.onvif_auth_pass) - 1);
+ }
+ }
+ char escOnvifUser[sizeof(app_config.onvif_auth_user) * 6 + 1];
+ respLen = sprintf(response,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: application/json;charset=UTF-8\r\n"
+ "Connection: close\r\n"
+ "\r\n"
+ "{\"enable\":%s,\"enable_auth\":%s,\"auth_user\":\"%s\",\"note\":\"applies after restart\"}",
+ app_config.onvif_enable ? "true" : "false", app_config.onvif_enable_auth ? "true" : "false",
+ json_str(escOnvifUser, sizeof(escOnvifUser), app_config.onvif_auth_user));
+ send_and_close(req->clntFd, response, respLen);
+ return;
+ }
if (EQUALS(req->uri, "/api/night")) {
if (req->query) {
char *remain;
@@ -1637,6 +1751,12 @@ void *server_thread(void *vargp) {
parse_request(req);
if (req->clntFd != -1) {
fcntl(req->clntFd, F_SETFL, fcntl(req->clntFd, F_GETFL, 0) & ~O_NONBLOCK);
+ {
+ /* A client that stops reading a stream (e.g. a browser that gave
+ * up on the MP4) must not block the whole server on send() */
+ struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
+ setsockopt(req->clntFd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
+ }
respond_request(req);
}
fds[i + 1].fd = -1;
diff --git a/src/server.h b/src/server.h
index 3d6d4e1e..b9c0524d 100644
--- a/src/server.h
+++ b/src/server.h
@@ -39,5 +39,6 @@ void send_jpeg_to_client(char index, char *buf, ssize_t size);
void send_mjpeg_to_client(char index, char *buf, ssize_t size);
void send_h26x_to_client(char index, hal_vidstream *stream);
void send_mp3_to_client(char *buf, ssize_t size);
+char http_audio_clients(void);
void send_mp4_to_client(char index, hal_vidstream *stream, char isH265);
void send_pcm_to_client(hal_audframe *frame);
From c8b0e016a026ff3cc850f010ef209d2b05c23dd9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?William=20B=C3=A9rub=C3=A9?=
Date: Wed, 9 Sep 2026 03:59:25 +0000
Subject: [PATCH 2/2] Sharing JSON escaping and stream flags, adding G.711
Moving the ranged value parser and JSON escaping to hal/tools so any module
can reuse them, and giving the stream types power-of-two values so future
codecs slot in as bit flags.
Extending the RTSP audio path with G.711 alongside MP3, sharing one
resampler and encoder shape for both A-law and mu-law. Keeping the codec
latched at startup and exposing it through the configuration and a new
Network section of the web UI, and accepting either flavor wherever the
codec is validated.
Leaving bitbuf.c as-is: at -O2 the compiler already emits memcpy for that
loop, under -Os it is not guaranteed to beat it, and it loses the safe
zero-length handling. Not worth touching a shared, verbatim module for a
performance-neutral change.
---
res/index.html | 124 ++++++++++++++++++++++++++++++-----------
src/app_config.c | 4 +-
src/app_config.h | 2 +-
src/hal/tools.c | 38 +++++++++++++
src/hal/tools.h | 4 ++
src/media.c | 66 +++++++++++-----------
src/rtsp/rtp.c | 36 ++++++------
src/rtsp/rtsp_server.h | 1 +
src/server.c | 53 ++++++------------
src/server.h | 3 +-
10 files changed, 207 insertions(+), 124 deletions(-)
diff --git a/res/index.html b/res/index.html
index 78df54c9..dd33458b 100644
--- a/res/index.html
+++ b/res/index.html
@@ -368,12 +368,12 @@
function onLoad() {
apiRead('audio');
apiRead('jpeg');
- apiRead('mp4');
apiRead('mjpeg');
+ apiRead('mp4');
apiRead('night');
+ apiRead('onvif');
apiRead('record');
apiRead('rtsp');
- apiRead('onvif');
toggleRecord(document.getElementById('record_state'));
apiRead('time');
@@ -678,37 +678,6 @@ Media