From 63318a7e486a6646a1977455b9200751df7e2d35 Mon Sep 17 00:00:00 2001 From: cpq Date: Wed, 1 Apr 2026 13:37:21 +0100 Subject: [PATCH] Add RTSP example --- tutorials/tcp/rtsp-video-stream/Makefile | 25 + tutorials/tcp/rtsp-video-stream/main.c | 28 + tutorials/tcp/rtsp-video-stream/mongoose.c | 1 + tutorials/tcp/rtsp-video-stream/mongoose.h | 1 + tutorials/tcp/rtsp-video-stream/rtsp.c | 2089 ++++++++++++++++++++ tutorials/tcp/rtsp-video-stream/rtsp.h | 54 + tutorials/tcp/rtsp-video-stream/test.mp4 | Bin 0 -> 20507 bytes 7 files changed, 2198 insertions(+) create mode 100644 tutorials/tcp/rtsp-video-stream/Makefile create mode 100644 tutorials/tcp/rtsp-video-stream/main.c create mode 120000 tutorials/tcp/rtsp-video-stream/mongoose.c create mode 120000 tutorials/tcp/rtsp-video-stream/mongoose.h create mode 100644 tutorials/tcp/rtsp-video-stream/rtsp.c create mode 100644 tutorials/tcp/rtsp-video-stream/rtsp.h create mode 100644 tutorials/tcp/rtsp-video-stream/test.mp4 diff --git a/tutorials/tcp/rtsp-video-stream/Makefile b/tutorials/tcp/rtsp-video-stream/Makefile new file mode 100644 index 00000000..e9c70119 --- /dev/null +++ b/tutorials/tcp/rtsp-video-stream/Makefile @@ -0,0 +1,25 @@ +PROG ?= example # Program we are building +DELETE = rm -rf # Command to remove files +OUT ?= -o $(PROG) # Compiler argument for output file +SOURCES = main.c mongoose.c rtsp.c # Source code files +CFLAGS = -W -Wall -Wextra -g -I. -include rtsp.h # Build options + +# Mongoose build options. See https://mongoose.ws/documentation/#build-options +#CFLAGS_MONGOOSE += -DMG_ENABLE_LINES + +ifeq ($(OS),Windows_NT) # Windows settings. Assume MinGW compiler. To use VC: make CC=cl CFLAGS=/MD OUT=/Feprog.exe + PROG ?= example.exe # Use .exe suffix for the binary + CC = gcc # Use MinGW gcc compiler + CFLAGS += -lws2_32 # Link against Winsock library + DELETE = cmd /C del /Q /F /S # Command prompt command to delete files + OUT ?= -o $(PROG) # Build output +endif + +all: $(PROG) # Default target. Build and run program + $(RUN) ./$(PROG) $(ARGS) + +$(PROG): $(SOURCES) # Build program from sources + $(CC) $(SOURCES) $(CFLAGS) $(CFLAGS_MONGOOSE) $(CFLAGS_EXTRA) $(OUT) + +clean: # Cleanup. Delete built program and all build artifacts + $(DELETE) $(PROG) *.o *.obj *.exe *.dSYM diff --git a/tutorials/tcp/rtsp-video-stream/main.c b/tutorials/tcp/rtsp-video-stream/main.c new file mode 100644 index 00000000..a41adf33 --- /dev/null +++ b/tutorials/tcp/rtsp-video-stream/main.c @@ -0,0 +1,28 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include "mongoose.h" + +int main(void) { + struct mg_mgr mgr; + + struct mg_rtsp_opts opts = { + 0, + 0, + 0, + mg_rtsp_get_mp4_frame, + (void *) "test.mp4", + mg_rtsp_get_mp4_audio, + (void *) "test.mp4", + {0x12, 0x10}, + }; + + mg_mgr_init(&mgr); + mg_rtsp_listen(&mgr, "rtsp://0.0.0.0:8554/cam", &opts); + + while (true) { + mg_mgr_poll(&mgr, 10); + } + mg_mgr_free(&mgr); + return 0; +} diff --git a/tutorials/tcp/rtsp-video-stream/mongoose.c b/tutorials/tcp/rtsp-video-stream/mongoose.c new file mode 120000 index 00000000..5e522bbc --- /dev/null +++ b/tutorials/tcp/rtsp-video-stream/mongoose.c @@ -0,0 +1 @@ +../../../mongoose.c \ No newline at end of file diff --git a/tutorials/tcp/rtsp-video-stream/mongoose.h b/tutorials/tcp/rtsp-video-stream/mongoose.h new file mode 120000 index 00000000..ee4ac823 --- /dev/null +++ b/tutorials/tcp/rtsp-video-stream/mongoose.h @@ -0,0 +1 @@ +../../../mongoose.h \ No newline at end of file diff --git a/tutorials/tcp/rtsp-video-stream/rtsp.c b/tutorials/tcp/rtsp-video-stream/rtsp.c new file mode 100644 index 00000000..cc2d9955 --- /dev/null +++ b/tutorials/tcp/rtsp-video-stream/rtsp.c @@ -0,0 +1,2089 @@ +#include "rtsp.h" + +#define RTSP_TRACK_PREFIX "/trackID=" +#define RTSP_VIDEO_TRACK 0U +#define RTSP_AUDIO_TRACK 1U +#define RTSP_TRACK_COUNT 2U +#define RTSP_VIDEO_RTP_PT 96U +#define RTSP_AUDIO_RTP_PT 97U +#define RTSP_VIDEO_RTP_CLOCK 90000U +#define RTSP_RTP_PAYLOAD_MAX 1400U +#define RTSP_MAX_NALS 16U + +#define H264_MAX_AU (4U * 1024U * 1024U) +#define H264_MAX_RBSP (4U * 1024U * 1024U) + +struct rtsp_request { + struct mg_str method; + struct mg_str uri; + struct mg_str cseq; + struct mg_str transport; + struct mg_str session; + size_t msg_len; +}; + +struct rtsp_nal { + const uint8_t *ptr; + size_t len; +}; + +struct rtsp_server { + struct mg_rtsp_opts opts; + char *path; + bool has_audio; + char sdp[1024]; +}; + +struct rtsp_track_conn { + bool setup_done; + bool ended; + uint16_t rtp_seq; + uint32_t ssrc; + unsigned rtp_channel; + unsigned rtcp_channel; + uint64_t deadline_ms; +}; + +struct rtsp_conn { + struct rtsp_server *srv; + bool playing; + struct rtsp_track_conn tracks[RTSP_TRACK_COUNT]; + char session_id[24]; +}; + +struct h264_bw { + uint8_t *buf; + size_t len; + size_t cap; + unsigned bitpos; + bool oom; +}; + +struct h264_color { + uint8_t y; + uint8_t cb; + uint8_t cr; +}; + +struct h264_testsrc_state { + bool inited; + unsigned width; + unsigned height; + unsigned fps; + unsigned mb_width; + unsigned mb_height; + unsigned box_w; + unsigned box_h; + int box_x; + int box_y; + int dx; + int dy; + uint64_t frame_no; + uint8_t sps[64]; + uint8_t pps[32]; + size_t sps_len; + size_t pps_len; + uint8_t rbsp[H264_MAX_RBSP]; + uint8_t au[H264_MAX_AU]; +}; + +#define MP4_TAG(a, b, c, d) \ + (((uint32_t) (uint8_t) (a) << 24) | ((uint32_t) (uint8_t) (b) << 16) | \ + ((uint32_t) (uint8_t) (c) << 8) | (uint32_t) (uint8_t) (d)) + +struct mp4_box { + const uint8_t *buf; + size_t len; + uint32_t type; +}; + +struct rtsp_mp4_stsc { + uint32_t first_chunk; + uint32_t samples_per_chunk; +}; + +struct rtsp_mp4_sample { + uint64_t offset; + uint32_t size; + uint64_t ts; + uint64_t pts_us; + bool keyframe; +}; + +struct rtsp_mp4_state { + struct rtsp_mp4_state *next; + char *path; + struct mg_str file; + struct rtsp_mp4_sample *samples; + size_t sample_count; + size_t start_index; + size_t sample_index; + uint32_t width; + uint32_t height; + unsigned fps; + uint32_t timescale; + uint8_t nal_len_size; + uint8_t *param_sets; + size_t param_sets_len; + uint8_t *sps; + size_t sps_len; + uint8_t *pps; + size_t pps_len; + uint8_t *au; + size_t au_cap; + struct rtsp_mp4_sample *audio_samples; + size_t audio_sample_count; + size_t audio_sample_index; + uint32_t audio_timescale; + unsigned audio_rate; + unsigned audio_channels; + uint8_t *audio_config; + size_t audio_config_len; +}; + +static struct rtsp_mp4_state *s_rtsp_mp4_states; + +static int rtsp_tolower(int ch) { + return ch >= 'A' && ch <= 'Z' ? ch + ('a' - 'A') : ch; +} + +static bool rtsp_memeq(const void *ptr1, const void *ptr2, size_t len) { + return len == 0 || memcmp(ptr1, ptr2, len) == 0; +} + +static bool rtsp_caseeq(struct mg_str s, const char *lit) { + size_t i, len = strlen(lit); + if (s.len != len) return false; + for (i = 0; i < len; i++) { + if (rtsp_tolower((unsigned char) s.buf[i]) != rtsp_tolower((unsigned char) lit[i])) { + return false; + } + } + return true; +} + +static bool rtsp_casencontains(struct mg_str s, const char *lit) { + size_t i, j, slen = s.len, llen = strlen(lit); + if (llen == 0 || llen > slen) return false; + for (i = 0; i + llen <= slen; i++) { + for (j = 0; j < llen; j++) { + if (rtsp_tolower((unsigned char) s.buf[i + j]) != + rtsp_tolower((unsigned char) lit[j])) { + break; + } + } + if (j == llen) return true; + } + return false; +} + +static struct mg_str rtsp_trim(struct mg_str s) { + while (s.len > 0 && + (s.buf[0] == ' ' || s.buf[0] == '\t' || s.buf[0] == '\r' || + s.buf[0] == '\n')) { + s.buf++; + s.len--; + } + while (s.len > 0 && + (s.buf[s.len - 1] == ' ' || s.buf[s.len - 1] == '\t' || + s.buf[s.len - 1] == '\r' || s.buf[s.len - 1] == '\n')) { + s.len--; + } + return s; +} + +static bool rtsp_parse_u32(struct mg_str s, uint32_t *value) { + size_t i; + uint32_t n = 0; + s = rtsp_trim(s); + if (s.len == 0) return false; + for (i = 0; i < s.len; i++) { + unsigned ch = (unsigned char) s.buf[i]; + if (ch < '0' || ch > '9') return false; + n = n * 10U + (uint32_t) (ch - '0'); + } + *value = n; + return true; +} + +static char *rtsp_strdup(const char *s) { + size_t len = strlen(s); + char *copy = (char *) mg_calloc(len + 1U, 1U); + if (copy != NULL) memcpy(copy, s, len); + return copy; +} + +static struct mg_str rtsp_uri_path(struct mg_str uri) { + const char *p = uri.buf, *end = uri.buf + uri.len, *scheme = NULL; + size_t i; + + for (i = 0; i + 2 < uri.len; i++) { + if (uri.buf[i] == ':' && uri.buf[i + 1] == '/' && uri.buf[i + 2] == '/') { + scheme = uri.buf + i; + break; + } + } + if (scheme != NULL) { + p = scheme + 3; + while (p < end && *p != '/') p++; + if (p >= end) return mg_str("/"); + } + if (p < end && *p != '/') { + while (p < end && *p != '/') p++; + if (p >= end) return mg_str("/"); + } + if (p >= end) return mg_str("/"); + end = p; + while (end < uri.buf + uri.len && *end != '?') end++; + return mg_str_n(p, (size_t) (end - p)); +} + +static bool rtsp_path_eq(struct mg_str uri, const char *path) { + struct mg_str upath = rtsp_uri_path(uri); + size_t path_len = strlen(path); + return upath.len == path_len && rtsp_memeq(upath.buf, path, path_len); +} + +static int rtsp_track_path_eq(struct mg_str uri, const char *path) { + struct mg_str upath = rtsp_uri_path(uri); + struct mg_str id; + uint32_t track_id; + size_t path_len = strlen(path), prefix_len = strlen(RTSP_TRACK_PREFIX); + + if (upath.len <= path_len + prefix_len) return -1; + if (!rtsp_memeq(upath.buf, path, path_len) || + !rtsp_memeq(upath.buf + path_len, RTSP_TRACK_PREFIX, prefix_len)) { + return -1; + } + id = mg_str_n(upath.buf + path_len + prefix_len, + upath.len - path_len - prefix_len); + if (!rtsp_parse_u32(id, &track_id)) return -1; + return (int) track_id; +} + +static bool rtsp_next_line(const char **cursor, const char *end, + struct mg_str *line) { + const char *p = *cursor; + if (p >= end) return false; + while (p < end && *p != '\n') p++; + if (p >= end) return false; + line->buf = (char *) *cursor; + line->len = (size_t) (p - *cursor); + if (line->len > 0 && line->buf[line->len - 1] == '\r') line->len--; + *cursor = p + 1; + return true; +} + +static int rtsp_parse_request(const char *buf, size_t len, + struct rtsp_request *req) { + const char *cursor = buf, *end; + struct mg_str line; + int header_len = mg_http_get_request_len((const unsigned char *) buf, len); + uint32_t content_length = 0; + + memset(req, 0, sizeof(*req)); + if (header_len <= 0) return header_len; + if ((size_t) header_len > len) return 0; + end = buf + header_len; + if (!rtsp_next_line(&cursor, end, &line)) return -1; + + { + const char *p = line.buf, *line_end = line.buf + line.len; + while (p < line_end && *p == ' ') p++; + req->method.buf = (char *) p; + while (p < line_end && *p != ' ') p++; + req->method.len = (size_t) (p - req->method.buf); + while (p < line_end && *p == ' ') p++; + req->uri.buf = (char *) p; + while (p < line_end && *p != ' ') p++; + req->uri.len = (size_t) (p - req->uri.buf); + while (p < line_end && *p == ' ') p++; + if (!rtsp_caseeq(mg_str_n(p, (size_t) (line_end - p)), "RTSP/1.0")) return -1; + if (req->method.len == 0 || req->uri.len == 0) return -1; + } + + while (rtsp_next_line(&cursor, end, &line)) { + struct mg_str name, value; + char *colon; + if (line.len == 0) break; + colon = (char *) memchr(line.buf, ':', line.len); + if (colon == NULL) return -1; + name = mg_str_n(line.buf, (size_t) (colon - line.buf)); + value = rtsp_trim(mg_str_n(colon + 1, line.len - (size_t) (colon - line.buf) - 1)); + if (rtsp_caseeq(name, "CSeq")) { + req->cseq = value; + } else if (rtsp_caseeq(name, "Transport")) { + req->transport = value; + } else if (rtsp_caseeq(name, "Session")) { + req->session = value; + } else if (rtsp_caseeq(name, "Content-Length")) { + if (!rtsp_parse_u32(value, &content_length)) return -1; + } + } + + req->msg_len = (size_t) header_len + (size_t) content_length; + if (req->msg_len > len) return 0; + return req->cseq.len > 0 ? 1 : -1; +} + +static void rtsp_send_response(struct mg_connection *c, + const struct rtsp_request *req, int status, + const char *reason, const char *headers, + const void *body, size_t body_len) { + const char *extra = headers == NULL ? "" : headers; + mg_printf(c, + "RTSP/1.0 %d %s\r\n" + "CSeq: %.*s\r\n" + "Server: Mongoose RTSP\r\n" + "%s" + "Content-Length: %lu\r\n" + "\r\n", + status, reason, (int) req->cseq.len, req->cseq.buf, extra, + (unsigned long) body_len); + if (body_len > 0) mg_send(c, body, body_len); +} + +static void rtsp_send_status(struct mg_connection *c, + const struct rtsp_request *req, int status, + const char *reason, const char *headers) { + rtsp_send_response(c, req, status, reason, headers, NULL, 0); +} + +static size_t h264_start_code_len(const uint8_t *buf, size_t len, size_t off) { + if (off + 3 <= len && buf[off] == 0 && buf[off + 1] == 0 && + buf[off + 2] == 1) { + return 3; + } + if (off + 4 <= len && buf[off] == 0 && buf[off + 1] == 0 && + buf[off + 2] == 0 && buf[off + 3] == 1) { + return 4; + } + return 0; +} + +static size_t h264_split_annexb(const uint8_t *buf, size_t len, + struct rtsp_nal *nals, size_t max_nals) { + size_t i = 0, count = 0; + + while (i < len) { + size_t sc = h264_start_code_len(buf, len, i); + if (sc == 0) { + i++; + continue; + } + i += sc; + if (i >= len) break; + { + size_t start = i; + while (i < len && h264_start_code_len(buf, len, i) == 0) i++; + if (start < i && count < max_nals) { + nals[count].ptr = buf + start; + nals[count].len = i - start; + count++; + } + } + } + + if (count == 0 && len > 0 && max_nals > 0) { + nals[0].ptr = buf; + nals[0].len = len; + count = 1; + } + return count; +} + +static void rtsp_parse_interleaved_channels(struct mg_str transport, + unsigned *rtp_channel, + unsigned *rtcp_channel) { + size_t i; + *rtp_channel = 0; + *rtcp_channel = 1; + for (i = 0; i + 11 < transport.len; i++) { + if (rtsp_tolower((unsigned char) transport.buf[i]) == 'i' && + rtsp_tolower((unsigned char) transport.buf[i + 1]) == 'n' && + rtsp_tolower((unsigned char) transport.buf[i + 2]) == 't' && + rtsp_tolower((unsigned char) transport.buf[i + 3]) == 'e' && + rtsp_tolower((unsigned char) transport.buf[i + 4]) == 'r' && + rtsp_tolower((unsigned char) transport.buf[i + 5]) == 'l' && + rtsp_tolower((unsigned char) transport.buf[i + 6]) == 'e' && + rtsp_tolower((unsigned char) transport.buf[i + 7]) == 'a' && + rtsp_tolower((unsigned char) transport.buf[i + 8]) == 'v' && + rtsp_tolower((unsigned char) transport.buf[i + 9]) == 'e' && + rtsp_tolower((unsigned char) transport.buf[i + 10]) == 'd' && + transport.buf[i + 11] == '=') { + uint32_t a = 0, b = 1; + size_t j = i + 12; + struct mg_str s1, s2; + s1.buf = &transport.buf[j]; + s1.len = 0; + while (j < transport.len && transport.buf[j] >= '0' && transport.buf[j] <= '9') { + s1.len++; + j++; + } + if (j >= transport.len || transport.buf[j] != '-') return; + j++; + s2.buf = &transport.buf[j]; + s2.len = 0; + while (j < transport.len && transport.buf[j] >= '0' && transport.buf[j] <= '9') { + s2.len++; + j++; + } + if (rtsp_parse_u32(s1, &a) && rtsp_parse_u32(s2, &b)) { + *rtp_channel = (unsigned) a; + *rtcp_channel = (unsigned) b; + } + return; + } + } +} + +static bool rtsp_session_matches(const struct rtsp_conn *rc, struct mg_str session) { + size_t i; + session = rtsp_trim(session); + for (i = 0; i < session.len; i++) { + if (session.buf[i] == ';') { + session.len = i; + break; + } + } + return session.len == strlen(rc->session_id) && + rtsp_memeq(session.buf, rc->session_id, session.len); +} + +static uint16_t rtsp_mp4_u16(const uint8_t *p) { + return (uint16_t) (((uint16_t) p[0] << 8) | p[1]); +} + +static uint32_t rtsp_mp4_u32(const uint8_t *p) { + return ((uint32_t) p[0] << 24) | ((uint32_t) p[1] << 16) | + ((uint32_t) p[2] << 8) | p[3]; +} + +static uint64_t rtsp_mp4_u64(const uint8_t *p) { + return ((uint64_t) rtsp_mp4_u32(p) << 32) | rtsp_mp4_u32(p + 4); +} + +static uint64_t rtsp_scale_us(uint64_t value, uint32_t timescale) { + if (timescale == 0) return 0; + return (value / timescale) * 1000000ULL + + (((value % timescale) * 1000000ULL) + timescale / 2U) / timescale; +} + +static bool rtsp_mp4_next_box(const uint8_t *buf, size_t len, size_t *ofs, + struct mp4_box *box) { + uint64_t start = *ofs, size, head = 8; + if (start + 8U > len) return false; + size = rtsp_mp4_u32(buf + start); + box->type = rtsp_mp4_u32(buf + start + 4U); + if (size == 1U) { + if (start + 16U > len) return false; + size = rtsp_mp4_u64(buf + start + 8U); + head = 16; + } else if (size == 0) { + size = len - start; + } + if (size < head || start + size > len) return false; + box->buf = buf + start + head; + box->len = (size_t) (size - head); + *ofs = (size_t) (start + size); + return true; +} + +static bool rtsp_mp4_find_child(const uint8_t *buf, size_t len, uint32_t type, + struct mp4_box *box) { + size_t ofs = 0; + struct mp4_box tmp; + while (rtsp_mp4_next_box(buf, len, &ofs, &tmp)) { + if (tmp.type == type) { + *box = tmp; + return true; + } + } + return false; +} + +static uint32_t rtsp_mp4_track_type(const struct mp4_box *mdia) { + struct mp4_box hdlr; + return rtsp_mp4_find_child(mdia->buf, mdia->len, MP4_TAG('h', 'd', 'l', 'r'), + &hdlr) && + hdlr.len >= 12U + ? rtsp_mp4_u32(hdlr.buf + 8U) + : 0; +} + +static void rtsp_mp4_free_state(struct rtsp_mp4_state *st) { + if (st == NULL) return; + mg_free(st->path); + mg_free(st->file.buf); + mg_free(st->samples); + mg_free(st->audio_samples); + mg_free(st->param_sets); + mg_free(st->sps); + mg_free(st->pps); + mg_free(st->au); + mg_free(st->audio_config); + mg_free(st); +} + +static bool rtsp_mp4_copy_blob(uint8_t **dst, size_t *dst_len, const uint8_t *src, + size_t len) { + uint8_t *copy = (uint8_t *) mg_calloc(len == 0 ? 1U : len, 1U); + if (copy == NULL) return false; + if (len > 0) memcpy(copy, src, len); + mg_free(*dst); + *dst = copy; + *dst_len = len; + return true; +} + +static bool rtsp_mp4_parse_avcc(struct rtsp_mp4_state *st, const uint8_t *buf, + size_t len) { + size_t i, pos, out_len = 0, out_cap; + uint8_t num_pps; + uint8_t *out; + if (len < 7U) return false; + + st->nal_len_size = (uint8_t) ((buf[4] & 3U) + 1U); + out_cap = len * 2U + 32U; + out = (uint8_t *) mg_calloc(out_cap, 1U); + if (out == NULL) return false; + + pos = 6U; + for (i = 0; i < (size_t) (buf[5] & 0x1fU); i++) { + uint16_t n; + if (pos + 2U > len) goto fail; + n = rtsp_mp4_u16(buf + pos); + pos += 2U; + if (n == 0 || pos + n > len || out_len + 4U + n > out_cap) goto fail; + if (st->sps == NULL && !rtsp_mp4_copy_blob(&st->sps, &st->sps_len, buf + pos, n)) + goto fail; + memcpy(out + out_len, "\0\0\0\1", 4U); + out_len += 4U; + memcpy(out + out_len, buf + pos, n); + out_len += n; + pos += n; + } + if (pos + 1U > len) goto fail; + num_pps = buf[pos++]; + for (i = 0; i < num_pps; i++) { + uint16_t n; + if (pos + 2U > len) goto fail; + n = rtsp_mp4_u16(buf + pos); + pos += 2U; + if (n == 0 || pos + n > len || out_len + 4U + n > out_cap) goto fail; + if (st->pps == NULL && !rtsp_mp4_copy_blob(&st->pps, &st->pps_len, buf + pos, n)) + goto fail; + memcpy(out + out_len, "\0\0\0\1", 4U); + out_len += 4U; + memcpy(out + out_len, buf + pos, n); + out_len += n; + pos += n; + } + if (st->sps == NULL || st->pps == NULL || out_len == 0) goto fail; + st->param_sets = out; + st->param_sets_len = out_len; + return true; + +fail: + mg_free(out); + return false; +} + +static bool rtsp_mp4_next_desc(const uint8_t *buf, size_t len, size_t *ofs, + uint8_t *tag, const uint8_t **body, + size_t *body_len) { + size_t pos = *ofs, i; + uint32_t size = 0; + + if (pos >= len) return false; + *tag = buf[pos++]; + for (i = 0; i < 4; i++) { + uint8_t b; + if (pos >= len) return false; + b = buf[pos++]; + size = (size << 7) | (uint32_t) (b & 0x7fU); + if ((b & 0x80U) == 0) break; + } + if (pos + size > len) return false; + *body = buf + pos; + *body_len = size; + *ofs = pos + size; + return true; +} + +static bool rtsp_mp4_parse_esds(struct rtsp_mp4_state *st, const uint8_t *buf, + size_t len) { + const uint8_t *body, *config; + size_t body_len, config_len, ofs; + uint8_t tag; + + if (len < 4U) return false; + ofs = 0; + if (!rtsp_mp4_next_desc(buf + 4U, len - 4U, &ofs, &tag, &body, &body_len) || + tag != 0x03U || body_len < 3U) { + return false; + } + ofs = 3U; + if ((body[2] & 0x80U) != 0) ofs += 2U; + if ((body[2] & 0x40U) != 0) { + if (ofs >= body_len) return false; + ofs += 1U + body[ofs]; + } + if ((body[2] & 0x20U) != 0) ofs += 2U; + if (ofs > body_len || + !rtsp_mp4_next_desc(body, body_len, &ofs, &tag, &body, &body_len) || + tag != 0x04U || body_len < 13U) { + return false; + } + ofs = 13U; + if (!rtsp_mp4_next_desc(body, body_len, &ofs, &tag, &config, &config_len) || + tag != 0x05U || config_len == 0) { + return false; + } + while (config_len > 0 && config[config_len - 1] == 0) config_len--; + return config_len > 0 && + rtsp_mp4_copy_blob(&st->audio_config, &st->audio_config_len, config, + config_len); +} + +static bool rtsp_mp4_parse_stsd(struct rtsp_mp4_state *st, + const struct mp4_box *stsd) { + size_t ofs = 8U; + struct mp4_box entry, avcc; + if (stsd->len < 8U || rtsp_mp4_u32(stsd->buf + 4U) == 0) return false; + while (rtsp_mp4_next_box(stsd->buf, stsd->len, &ofs, &entry)) { + if (entry.type != MP4_TAG('a', 'v', 'c', '1') && + entry.type != MP4_TAG('a', 'v', 'c', '3')) { + continue; + } + if (entry.len < 78U) return false; + st->width = rtsp_mp4_u16(entry.buf + 24U); + st->height = rtsp_mp4_u16(entry.buf + 26U); + if (!rtsp_mp4_find_child(entry.buf + 78U, entry.len - 78U, + MP4_TAG('a', 'v', 'c', 'C'), &avcc)) { + return false; + } + return rtsp_mp4_parse_avcc(st, avcc.buf, avcc.len); + } + return false; +} + +static bool rtsp_mp4_parse_audio_stsd(struct rtsp_mp4_state *st, + const struct mp4_box *stsd) { + size_t ofs = 8U; + struct mp4_box entry, esds; + + if (stsd->len < 8U || rtsp_mp4_u32(stsd->buf + 4U) == 0) return false; + while (rtsp_mp4_next_box(stsd->buf, stsd->len, &ofs, &entry)) { + uint16_t version; + size_t child_ofs = 28U; + + if (entry.type != MP4_TAG('m', 'p', '4', 'a')) continue; + if (entry.len < 28U) return false; + version = rtsp_mp4_u16(entry.buf + 8U); + if (version == 1U) { + child_ofs = 44U; + } else if (version == 2U) { + child_ofs = 64U; + } + if (entry.len < child_ofs) return false; + st->audio_channels = rtsp_mp4_u16(entry.buf + 16U); + st->audio_rate = rtsp_mp4_u32(entry.buf + 24U) >> 16; + if (st->audio_channels == 0 || st->audio_rate == 0) return false; + if (!rtsp_mp4_find_child(entry.buf + child_ofs, entry.len - child_ofs, + MP4_TAG('e', 's', 'd', 's'), &esds)) { + return false; + } + return rtsp_mp4_parse_esds(st, esds.buf, esds.len); + } + return false; +} + +static bool rtsp_mp4_parse_mdhd(struct rtsp_mp4_state *st, + const struct mp4_box *mdhd) { + if (mdhd->len < 16U) return false; + if (mdhd->buf[0] == 1) { + if (mdhd->len < 24U) return false; + st->timescale = rtsp_mp4_u32(mdhd->buf + 20U); + } else { + st->timescale = rtsp_mp4_u32(mdhd->buf + 12U); + } + return st->timescale > 0; +} + +static bool rtsp_mp4_parse_audio_mdhd(struct rtsp_mp4_state *st, + const struct mp4_box *mdhd) { + if (mdhd->len < 16U) return false; + if (mdhd->buf[0] == 1) { + if (mdhd->len < 24U) return false; + st->audio_timescale = rtsp_mp4_u32(mdhd->buf + 20U); + } else { + st->audio_timescale = rtsp_mp4_u32(mdhd->buf + 12U); + } + return st->audio_timescale > 0; +} + +static uint32_t *rtsp_mp4_parse_stsz(const struct mp4_box *stsz, + size_t *sample_count) { + uint32_t fixed_size, count; + uint32_t *sizes; + size_t i; + if (stsz->len < 12U) return NULL; + fixed_size = rtsp_mp4_u32(stsz->buf + 4U); + count = rtsp_mp4_u32(stsz->buf + 8U); + if (count == 0) return NULL; + if (fixed_size == 0 && stsz->len < 12U + (size_t) count * 4U) return NULL; + + sizes = (uint32_t *) mg_calloc(count, sizeof(*sizes)); + if (sizes == NULL) return NULL; + if (fixed_size != 0) { + for (i = 0; i < count; i++) sizes[i] = fixed_size; + } else { + for (i = 0; i < count; i++) sizes[i] = rtsp_mp4_u32(stsz->buf + 12U + i * 4U); + } + *sample_count = count; + return sizes; +} + +static uint64_t *rtsp_mp4_parse_chunk_offsets(const struct mp4_box *box, + size_t *count) { + uint32_t n; + size_t i; + uint64_t *offsets; + bool is64 = box->type == MP4_TAG('c', 'o', '6', '4'); + if (box->len < 8U) return NULL; + n = rtsp_mp4_u32(box->buf + 4U); + if (n == 0) return NULL; + if (is64) { + if (box->len < 8U + (size_t) n * 8U) return NULL; + } else { + if (box->len < 8U + (size_t) n * 4U) return NULL; + } + offsets = (uint64_t *) mg_calloc(n, sizeof(*offsets)); + if (offsets == NULL) return NULL; + for (i = 0; i < n; i++) { + offsets[i] = + is64 ? rtsp_mp4_u64(box->buf + 8U + i * 8U) + : (uint64_t) rtsp_mp4_u32(box->buf + 8U + i * 4U); + } + *count = n; + return offsets; +} + +static struct rtsp_mp4_stsc *rtsp_mp4_parse_stsc(const struct mp4_box *stsc, + size_t *count) { + uint32_t n; + size_t i; + struct rtsp_mp4_stsc *entries; + if (stsc->len < 8U) return NULL; + n = rtsp_mp4_u32(stsc->buf + 4U); + if (n == 0 || stsc->len < 8U + (size_t) n * 12U) return NULL; + entries = (struct rtsp_mp4_stsc *) mg_calloc(n, sizeof(*entries)); + if (entries == NULL) return NULL; + for (i = 0; i < n; i++) { + const uint8_t *p = stsc->buf + 8U + i * 12U; + entries[i].first_chunk = rtsp_mp4_u32(p); + entries[i].samples_per_chunk = rtsp_mp4_u32(p + 4U); + } + *count = n; + return entries; +} + +static int64_t *rtsp_mp4_parse_pts(const struct mp4_box *stts, + const struct mp4_box *ctts, + size_t sample_count, + uint64_t *duration_units) { + int64_t *pts; + uint64_t dts = 0; + uint32_t entry_count; + size_t i, j, idx = 0, pos; + (void) ctts; + if (stts->len < 8U) return NULL; + entry_count = rtsp_mp4_u32(stts->buf + 4U); + pts = (int64_t *) mg_calloc(sample_count, sizeof(*pts)); + if (pts == NULL) return NULL; + + pos = 8U; + for (i = 0; i < entry_count; i++, pos += 8U) { + uint32_t count, delta; + if (pos + 8U > stts->len) goto fail; + count = rtsp_mp4_u32(stts->buf + pos); + delta = rtsp_mp4_u32(stts->buf + pos + 4U); + for (j = 0; j < count; j++) { + if (idx >= sample_count) goto fail; + pts[idx++] = (int64_t) dts; + dts += delta; + } + } + if (idx != sample_count) goto fail; + *duration_units = dts; + return pts; + +fail: + mg_free(pts); + return NULL; +} + +static bool rtsp_mp4_parse_samples(struct rtsp_mp4_state *st, + const struct mp4_box *stbl) { + struct mp4_box stsz, stsc, stts, ctts, stss, chunk_box; + uint32_t *sizes = NULL; + uint64_t *chunk_offsets = NULL, duration_units = 0; + struct rtsp_mp4_stsc *stsc_entries = NULL; + int64_t *pts = NULL, min_pts; + size_t sample_count = 0, chunk_count = 0, stsc_count = 0; + size_t i, chunk = 0, sample = 0, start_index = 0, active_count; + uint64_t duration_us; + bool has_ctts, has_stss; + + if (!rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 's', 'z'), + &stsz) || + !rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 's', 'c'), + &stsc) || + !rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 't', 's'), + &stts)) { + return false; + } + has_ctts = + rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('c', 't', 't', 's'), + &ctts); + has_stss = + rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 's', 's'), + &stss); + if (!rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 'c', 'o'), + &chunk_box) && + !rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('c', 'o', '6', '4'), + &chunk_box)) { + return false; + } + + sizes = rtsp_mp4_parse_stsz(&stsz, &sample_count); + chunk_offsets = rtsp_mp4_parse_chunk_offsets(&chunk_box, &chunk_count); + stsc_entries = rtsp_mp4_parse_stsc(&stsc, &stsc_count); + pts = rtsp_mp4_parse_pts(&stts, has_ctts ? &ctts : NULL, sample_count, + &duration_units); + st->samples = (struct rtsp_mp4_sample *) mg_calloc(sample_count, sizeof(*st->samples)); + if (sizes == NULL || chunk_offsets == NULL || stsc_entries == NULL || pts == NULL || + st->samples == NULL) { + goto fail; + } + + for (i = 0; i < stsc_count; i++) { + uint32_t first_chunk = stsc_entries[i].first_chunk; + uint32_t last_chunk = + i + 1U < stsc_count ? stsc_entries[i + 1U].first_chunk - 1U + : (uint32_t) chunk_count; + uint32_t chunk_no; + if (first_chunk == 0 || first_chunk > last_chunk) goto fail; + for (chunk_no = first_chunk; chunk_no <= last_chunk; chunk_no++) { + uint64_t offset; + uint32_t sample_no; + if ((size_t) chunk_no > chunk_count) goto fail; + offset = chunk_offsets[chunk_no - 1U]; + for (sample_no = 0; sample_no < stsc_entries[i].samples_per_chunk; sample_no++) { + if (sample >= sample_count) goto fail; + st->samples[sample].offset = offset; + st->samples[sample].size = sizes[sample]; + if (offset > st->file.len || sizes[sample] > st->file.len - (size_t) offset) + goto fail; + offset += sizes[sample]; + sample++; + } + chunk++; + } + } + if (sample != sample_count || chunk != chunk_count) goto fail; + + for (i = 0; i < sample_count; i++) st->samples[i].keyframe = !has_stss; + if (has_stss) { + uint32_t sync_count; + size_t pos = 8U; + if (stss.len < 8U) goto fail; + sync_count = rtsp_mp4_u32(stss.buf + 4U); + for (i = 0; i < sync_count; i++, pos += 4U) { + uint32_t sample_no; + if (pos + 4U > stss.len) goto fail; + sample_no = rtsp_mp4_u32(stss.buf + pos); + if (sample_no > 0 && sample_no <= sample_count) + st->samples[sample_no - 1U].keyframe = true; + } + } + + min_pts = pts[0]; + for (i = 0; i < sample_count; i++) { + if (pts[i] < min_pts) min_pts = pts[i]; + } + for (i = 0; i < sample_count; i++) { + st->samples[i].ts = (uint64_t) (pts[i] - min_pts); + st->samples[i].pts_us = rtsp_scale_us(st->samples[i].ts, st->timescale); + } + + while (start_index < sample_count && !st->samples[start_index].keyframe) start_index++; + if (start_index >= sample_count) start_index = 0; + + st->sample_count = sample_count; + st->start_index = start_index; + st->sample_index = start_index; + duration_us = + rtsp_scale_us(duration_units, st->timescale) > st->samples[start_index].pts_us + ? rtsp_scale_us(duration_units, st->timescale) - + st->samples[start_index].pts_us + : rtsp_scale_us(duration_units, st->timescale); + active_count = sample_count - start_index; + if (active_count == 0) active_count = sample_count; + if (duration_us == 0) duration_us = 1000000ULL / 30ULL; + st->fps = (unsigned) ((active_count * 1000000ULL + duration_us / 2U) / duration_us); + if (st->fps == 0) st->fps = 30U; + + mg_free(sizes); + mg_free(chunk_offsets); + mg_free(stsc_entries); + mg_free(pts); + return true; + +fail: + mg_free(sizes); + mg_free(chunk_offsets); + mg_free(stsc_entries); + mg_free(pts); + mg_free(st->samples); + st->samples = NULL; + return false; +} + +static bool rtsp_mp4_parse_audio_samples(struct rtsp_mp4_state *st, + const struct mp4_box *stbl) { + struct mp4_box stsz, stsc, stts, ctts, chunk_box; + uint32_t *sizes = NULL; + uint64_t *chunk_offsets = NULL, duration_units = 0; + struct rtsp_mp4_stsc *stsc_entries = NULL; + int64_t *pts = NULL, min_pts; + size_t sample_count = 0, chunk_count = 0, stsc_count = 0; + size_t i, chunk = 0, sample = 0; + bool has_ctts; + + if (!rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 's', 'z'), + &stsz) || + !rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 's', 'c'), + &stsc) || + !rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 't', 's'), + &stts)) { + return false; + } + has_ctts = + rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('c', 't', 't', 's'), + &ctts); + if (!rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('s', 't', 'c', 'o'), + &chunk_box) && + !rtsp_mp4_find_child(stbl->buf, stbl->len, MP4_TAG('c', 'o', '6', '4'), + &chunk_box)) { + return false; + } + + sizes = rtsp_mp4_parse_stsz(&stsz, &sample_count); + chunk_offsets = rtsp_mp4_parse_chunk_offsets(&chunk_box, &chunk_count); + stsc_entries = rtsp_mp4_parse_stsc(&stsc, &stsc_count); + pts = rtsp_mp4_parse_pts(&stts, has_ctts ? &ctts : NULL, sample_count, + &duration_units); + st->audio_samples = + (struct rtsp_mp4_sample *) mg_calloc(sample_count, sizeof(*st->audio_samples)); + if (sizes == NULL || chunk_offsets == NULL || stsc_entries == NULL || pts == NULL || + st->audio_samples == NULL) { + goto fail; + } + + for (i = 0; i < stsc_count; i++) { + uint32_t first_chunk = stsc_entries[i].first_chunk; + uint32_t last_chunk = + i + 1U < stsc_count ? stsc_entries[i + 1U].first_chunk - 1U + : (uint32_t) chunk_count; + uint32_t chunk_no; + if (first_chunk == 0 || first_chunk > last_chunk) goto fail; + for (chunk_no = first_chunk; chunk_no <= last_chunk; chunk_no++) { + uint64_t offset; + uint32_t sample_no; + if ((size_t) chunk_no > chunk_count) goto fail; + offset = chunk_offsets[chunk_no - 1U]; + for (sample_no = 0; sample_no < stsc_entries[i].samples_per_chunk; sample_no++) { + if (sample >= sample_count) goto fail; + st->audio_samples[sample].offset = offset; + st->audio_samples[sample].size = sizes[sample]; + st->audio_samples[sample].keyframe = true; + if (offset > st->file.len || sizes[sample] > st->file.len - (size_t) offset) { + goto fail; + } + offset += sizes[sample]; + sample++; + } + chunk++; + } + } + if (sample != sample_count || chunk != chunk_count) goto fail; + + min_pts = pts[0]; + for (i = 0; i < sample_count; i++) { + if (pts[i] < min_pts) min_pts = pts[i]; + } + for (i = 0; i < sample_count; i++) { + st->audio_samples[i].ts = (uint64_t) (pts[i] - min_pts); + st->audio_samples[i].pts_us = + rtsp_scale_us(st->audio_samples[i].ts, st->audio_timescale); + } + st->audio_sample_count = sample_count; + st->audio_sample_index = 0; + + mg_free(sizes); + mg_free(chunk_offsets); + mg_free(stsc_entries); + mg_free(pts); + return true; + +fail: + mg_free(sizes); + mg_free(chunk_offsets); + mg_free(stsc_entries); + mg_free(pts); + mg_free(st->audio_samples); + st->audio_samples = NULL; + return false; +} + +static bool rtsp_mp4_parse_file(struct rtsp_mp4_state *st) { + struct mp4_box root_moov, trak; + size_t ofs = 0; + if (!rtsp_mp4_find_child((const uint8_t *) st->file.buf, st->file.len, + MP4_TAG('m', 'o', 'o', 'v'), &root_moov)) { + return false; + } + while (rtsp_mp4_next_box(root_moov.buf, root_moov.len, &ofs, &trak)) { + struct mp4_box mdia, mdhd, minf, stbl, stsd; + uint32_t track_type; + if (trak.type != MP4_TAG('t', 'r', 'a', 'k')) continue; + if (!rtsp_mp4_find_child(trak.buf, trak.len, MP4_TAG('m', 'd', 'i', 'a'), + &mdia) || + !rtsp_mp4_find_child(mdia.buf, mdia.len, MP4_TAG('m', 'd', 'h', 'd'), + &mdhd) || + !rtsp_mp4_find_child(mdia.buf, mdia.len, MP4_TAG('m', 'i', 'n', 'f'), + &minf) || + !rtsp_mp4_find_child(minf.buf, minf.len, MP4_TAG('s', 't', 'b', 'l'), + &stbl) || + !rtsp_mp4_find_child(stbl.buf, stbl.len, MP4_TAG('s', 't', 's', 'd'), + &stsd)) { + continue; + } + track_type = rtsp_mp4_track_type(&mdia); + if (track_type == MP4_TAG('v', 'i', 'd', 'e') && st->samples == NULL) { + if (!rtsp_mp4_parse_mdhd(st, &mdhd) || !rtsp_mp4_parse_stsd(st, &stsd) || + !rtsp_mp4_parse_samples(st, &stbl)) { + return false; + } + } else if (track_type == MP4_TAG('s', 'o', 'u', 'n') && + st->audio_samples == NULL) { + if (!rtsp_mp4_parse_audio_mdhd(st, &mdhd) || + !rtsp_mp4_parse_audio_stsd(st, &stsd) || + !rtsp_mp4_parse_audio_samples(st, &stbl)) { + st->audio_rate = 0; + st->audio_channels = 0; + mg_free(st->audio_config); + st->audio_config = NULL; + st->audio_config_len = 0; + mg_free(st->audio_samples); + st->audio_samples = NULL; + st->audio_sample_count = 0; + st->audio_sample_index = 0; + } + } + } + return st->samples != NULL && st->sps != NULL && st->pps != NULL; +} + +static struct rtsp_mp4_state *rtsp_mp4_find_state(const char *path) { + struct rtsp_mp4_state *st; + for (st = s_rtsp_mp4_states; st != NULL; st = st->next) { + if (st->path != NULL && path != NULL && strcmp(st->path, path) == 0) return st; + } + return NULL; +} + +static struct rtsp_mp4_state *rtsp_mp4_get_state(const char *path) { + struct rtsp_mp4_state *st; + if (path == NULL) return NULL; + st = rtsp_mp4_find_state(path); + if (st != NULL) return st; + + st = (struct rtsp_mp4_state *) mg_calloc(1, sizeof(*st)); + if (st == NULL) return NULL; + st->path = rtsp_strdup(path); + if (st->path == NULL) goto fail; + st->file = mg_file_read(&mg_fs_posix, path); + if (st->file.buf == NULL || st->file.len == 0) goto fail; + if (!rtsp_mp4_parse_file(st)) goto fail; + st->next = s_rtsp_mp4_states; + s_rtsp_mp4_states = st; + return st; + +fail: + rtsp_mp4_free_state(st); + return NULL; +} + +static void rtsp_mp4_reset(const char *path) { + struct rtsp_mp4_state *st = rtsp_mp4_find_state(path); + if (st == NULL) return; + st->sample_index = st->start_index; + st->audio_sample_index = 0; +} + +static bool rtsp_mp4_ensure_au(struct rtsp_mp4_state *st, size_t cap) { + uint8_t *buf; + if (cap <= st->au_cap) return true; + buf = (uint8_t *) mg_calloc(cap, 1U); + if (buf == NULL) return false; + mg_free(st->au); + st->au = buf; + st->au_cap = cap; + return true; +} + +bool mg_rtsp_get_mp4_frame(struct mg_rtsp_frame *frame, void *fn_data) { + const char *path = (const char *) fn_data; + struct rtsp_mp4_state *st = rtsp_mp4_get_state(path); + struct rtsp_mp4_sample *sample; + const uint8_t *src; + size_t remaining, au_len = 0, prefix_len = 0; + + if (frame == NULL || st == NULL || st->sample_count == 0) return false; + if (st->sample_index >= st->sample_count) return false; + sample = &st->samples[st->sample_index]; + prefix_len = sample->keyframe ? st->param_sets_len : 0; + if (!rtsp_mp4_ensure_au(st, prefix_len + sample->size * 2U + 16U)) + return false; + + if (prefix_len > 0) { + memcpy(st->au, st->param_sets, prefix_len); + au_len = prefix_len; + } + + src = (const uint8_t *) st->file.buf + sample->offset; + remaining = sample->size; + while (remaining > (size_t) st->nal_len_size) { + uint32_t nal_len = 0; + unsigned i; + for (i = 0; i < st->nal_len_size; i++) nal_len = (nal_len << 8) | src[i]; + src += st->nal_len_size; + remaining -= st->nal_len_size; + if (nal_len == 0 || nal_len > remaining || au_len + 4U + nal_len > st->au_cap) + return false; + memcpy(st->au + au_len, "\0\0\0\1", 4U); + au_len += 4U; + memcpy(st->au + au_len, src, nal_len); + au_len += nal_len; + src += nal_len; + remaining -= nal_len; + } + if (au_len == 0) return false; + + frame->data = st->au; + frame->len = au_len; + frame->pts_us = sample->pts_us >= st->samples[st->start_index].pts_us + ? sample->pts_us - st->samples[st->start_index].pts_us + : 0; + frame->keyframe = sample->keyframe; + + st->sample_index++; + return true; +} + +bool mg_rtsp_get_mp4_audio(struct mg_rtsp_audio *audio, void *fn_data) { + const char *path = (const char *) fn_data; + struct rtsp_mp4_state *st = rtsp_mp4_get_state(path); + struct rtsp_mp4_sample *sample; + + if (audio == NULL || st == NULL || st->audio_samples == NULL || + st->audio_sample_count == 0 || st->audio_sample_index >= st->audio_sample_count) { + return false; + } + + sample = &st->audio_samples[st->audio_sample_index++]; + audio->data = (const uint8_t *) st->file.buf + sample->offset; + audio->len = sample->size; + audio->pts_us = sample->pts_us; + return true; +} + +static uint64_t rtsp_mp4_track_period_ms(const struct rtsp_mp4_sample *samples, + size_t sample_count, size_t sample_index, + uint64_t fallback_ms) { + uint64_t delta_us; + + if (samples == NULL || sample_count == 0 || sample_index >= sample_count) { + return fallback_ms == 0 ? 1U : fallback_ms; + } + if (sample_index + 1U < sample_count && + samples[sample_index + 1U].pts_us > samples[sample_index].pts_us) { + delta_us = samples[sample_index + 1U].pts_us - samples[sample_index].pts_us; + } else if (sample_index > 0 && + samples[sample_index].pts_us > samples[sample_index - 1U].pts_us) { + delta_us = samples[sample_index].pts_us - samples[sample_index - 1U].pts_us; + } else { + delta_us = (fallback_ms == 0 ? 1U : fallback_ms) * 1000ULL; + } + return (delta_us + 500ULL) / 1000ULL == 0 ? 1U : (delta_us + 500ULL) / 1000ULL; +} + +static void bw_init(struct h264_bw *bw, uint8_t *buf, size_t cap) { + memset(bw, 0, sizeof(*bw)); + bw->buf = buf; + bw->cap = cap; +} + +static void bw_put_bit(struct h264_bw *bw, unsigned bit) { + if (bw->oom) return; + if (bw->len >= bw->cap) { + bw->oom = true; + return; + } + if (bw->bitpos == 0) bw->buf[bw->len] = 0; + if (bit) bw->buf[bw->len] |= (uint8_t) (1U << (7U - bw->bitpos)); + bw->bitpos++; + if (bw->bitpos == 8) { + bw->bitpos = 0; + bw->len++; + } +} + +static void bw_put_bits(struct h264_bw *bw, unsigned count, uint32_t value) { + while (count > 0) { + count--; + bw_put_bit(bw, (value >> count) & 1U); + } +} + +static void bw_put_ue(struct h264_bw *bw, uint32_t value) { + uint32_t code_num = value + 1U; + unsigned leading_zero_bits = 0; + uint32_t tmp = code_num; + while ((tmp >>= 1U) != 0U) leading_zero_bits++; + { + unsigned i; + for (i = 0; i < leading_zero_bits; i++) bw_put_bit(bw, 0); + } + bw_put_bits(bw, leading_zero_bits + 1U, code_num); +} + +static void bw_put_se(struct h264_bw *bw, int32_t value) { + uint32_t code_num = value <= 0 ? (uint32_t) (-value * 2) : (uint32_t) (value * 2 - 1); + bw_put_ue(bw, code_num); +} + +static void bw_align_zero(struct h264_bw *bw) { + if (bw->oom) return; + if (bw->bitpos != 0) { + bw->bitpos = 0; + bw->len++; + } +} + +static void bw_put_byte(struct h264_bw *bw, uint8_t byte) { + if (bw->oom) return; + bw_align_zero(bw); + if (bw->len >= bw->cap) { + bw->oom = true; + return; + } + bw->buf[bw->len++] = byte; +} + +static void bw_rbsp_trailing_bits(struct h264_bw *bw) { + bw_put_bit(bw, 1U); + while (bw->bitpos != 0) bw_put_bit(bw, 0U); +} + +static size_t h264_nal_from_rbsp(uint8_t nal_header, const uint8_t *rbsp, + size_t rbsp_len, uint8_t *out, size_t out_cap) { + size_t i, len = 0, zeros = 0; + if (out_cap == 0) return 0; + out[len++] = nal_header; + for (i = 0; i < rbsp_len && len < out_cap; i++) { + uint8_t byte = rbsp[i]; + if (zeros >= 2 && byte <= 3) { + if (len >= out_cap) return 0; + out[len++] = 3; + zeros = 0; + } + if (len >= out_cap) return 0; + out[len++] = byte; + zeros = byte == 0 ? zeros + 1 : 0; + } + return i == rbsp_len ? len : 0; +} + +static bool h264_build_sps(struct h264_testsrc_state *st) { + struct h264_bw bw; + uint8_t rbsp[32]; + + bw_init(&bw, rbsp, sizeof(rbsp)); + bw_put_bits(&bw, 8, 66U); + bw_put_bit(&bw, 1U); + bw_put_bit(&bw, 1U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_bits(&bw, 2, 0U); + bw_put_bits(&bw, 8, 31U); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 2U); + bw_put_bit(&bw, 0U); + bw_put_ue(&bw, st->mb_width - 1U); + bw_put_ue(&bw, st->mb_height - 1U); + bw_put_bit(&bw, 1U); + bw_put_bit(&bw, 1U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_rbsp_trailing_bits(&bw); + if (bw.oom) return false; + + st->sps_len = h264_nal_from_rbsp(0x67U, rbsp, bw.len, st->sps, sizeof(st->sps)); + return st->sps_len > 0; +} + +static bool h264_build_pps(struct h264_testsrc_state *st) { + struct h264_bw bw; + uint8_t rbsp[16]; + + bw_init(&bw, rbsp, sizeof(rbsp)); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_bits(&bw, 2, 0U); + bw_put_se(&bw, 0); + bw_put_se(&bw, 0); + bw_put_se(&bw, 0); + bw_put_bit(&bw, 1U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_rbsp_trailing_bits(&bw); + if (bw.oom) return false; + + st->pps_len = h264_nal_from_rbsp(0x68U, rbsp, bw.len, st->pps, sizeof(st->pps)); + return st->pps_len > 0; +} + +static bool h264_testsrc_init(struct h264_testsrc_state *st, unsigned width, + unsigned height, unsigned fps) { + memset(st, 0, sizeof(*st)); + st->width = width == 0 ? 640U : width; + st->height = height == 0 ? 480U : height; + st->fps = fps == 0 ? 30U : fps; + st->mb_width = (st->width + 15U) / 16U; + st->mb_height = (st->height + 15U) / 16U; + st->box_w = st->mb_width / 8U; + st->box_h = st->mb_height / 8U; + if (st->box_w < 4U) st->box_w = 4U; + if (st->box_h < 4U) st->box_h = 4U; + if (st->box_w > st->mb_width) st->box_w = st->mb_width; + if (st->box_h > st->mb_height) st->box_h = st->mb_height; + st->dx = 1; + st->dy = 1; + if (!h264_build_sps(st) || !h264_build_pps(st)) return false; + st->inited = true; + return true; +} + +static void h264_append_start_code(uint8_t *buf, size_t *len) { + buf[(*len)++] = 0; + buf[(*len)++] = 0; + buf[(*len)++] = 0; + buf[(*len)++] = 1; +} + +static void h264_emit_pcm_macroblock(struct h264_bw *bw, + const struct h264_color *color) { + unsigned i; + bw_put_ue(bw, 25U); + bw_align_zero(bw); + for (i = 0; i < 16U * 16U; i++) bw_put_byte(bw, color->y); + for (i = 0; i < 8U * 8U; i++) bw_put_byte(bw, color->cb); + for (i = 0; i < 8U * 8U; i++) bw_put_byte(bw, color->cr); +} + +static bool h264_build_access_unit(struct h264_testsrc_state *st, + struct mg_rtsp_frame *frame) { + static const struct h264_color kGreen = {145U, 54U, 34U}; + static const struct h264_color kYellow = {210U, 16U, 146U}; + struct h264_bw bw; + uint8_t idr[H264_MAX_AU]; + size_t au_len = 0, idr_len; + unsigned mb_x, mb_y; + + bw_init(&bw, st->rbsp, sizeof(st->rbsp)); + bw_put_ue(&bw, 0U); + bw_put_ue(&bw, 2U); + bw_put_ue(&bw, 0U); + bw_put_bits(&bw, 4U, 0U); + bw_put_ue(&bw, 0U); + bw_put_bits(&bw, 4U, 0U); + bw_put_bit(&bw, 0U); + bw_put_bit(&bw, 0U); + bw_put_se(&bw, 0); + bw_put_ue(&bw, 1U); + + for (mb_y = 0; mb_y < st->mb_height; mb_y++) { + for (mb_x = 0; mb_x < st->mb_width; mb_x++) { + bool in_box = mb_x >= (unsigned) st->box_x && + mb_x < (unsigned) st->box_x + st->box_w && + mb_y >= (unsigned) st->box_y && + mb_y < (unsigned) st->box_y + st->box_h; + h264_emit_pcm_macroblock(&bw, in_box ? &kYellow : &kGreen); + } + } + bw_rbsp_trailing_bits(&bw); + if (bw.oom) return false; + + idr_len = h264_nal_from_rbsp(0x65U, st->rbsp, bw.len, idr, sizeof(idr)); + if (idr_len == 0) return false; + + h264_append_start_code(st->au, &au_len); + memcpy(st->au + au_len, st->sps, st->sps_len); + au_len += st->sps_len; + h264_append_start_code(st->au, &au_len); + memcpy(st->au + au_len, st->pps, st->pps_len); + au_len += st->pps_len; + h264_append_start_code(st->au, &au_len); + memcpy(st->au + au_len, idr, idr_len); + au_len += idr_len; + + frame->data = st->au; + frame->len = au_len; + frame->pts_us = (st->frame_no * 1000000ULL) / st->fps; + frame->keyframe = true; + st->frame_no++; + + st->box_x += st->dx; + st->box_y += st->dy; + if (st->box_x < 0) { + st->box_x = 0; + st->dx = 1; + } else if ((unsigned) st->box_x + st->box_w >= st->mb_width) { + st->box_x = (int) (st->mb_width - st->box_w); + st->dx = -1; + } + if (st->box_y < 0) { + st->box_y = 0; + st->dy = 1; + } else if ((unsigned) st->box_y + st->box_h >= st->mb_height) { + st->box_y = (int) (st->mb_height - st->box_h); + st->dy = -1; + } + return true; +} + +bool mg_rtsp_h264_testsrc_next_frame(struct mg_rtsp_frame *frame, unsigned width, + unsigned height, unsigned fps) { + static struct h264_testsrc_state st; + if (frame == NULL) return false; + if (!st.inited || st.width != width || st.height != height || st.fps != fps) { + if (!h264_testsrc_init(&st, width, height, fps)) return false; + } + return h264_build_access_unit(&st, frame); +} + +static size_t rtsp_hex_encode(const uint8_t *buf, size_t len, char *dst, + size_t dst_len) { + static const char *hex = "0123456789ABCDEF"; + size_t i, out = 0; + if (dst_len == 0) return 0; + for (i = 0; i < len && out + 2U < dst_len; i++) { + dst[out++] = hex[buf[i] >> 4]; + dst[out++] = hex[buf[i] & 0x0fU]; + } + dst[out] = '\0'; + return out; +} + +static bool rtsp_aac_lc_config_parse(const uint8_t cfg[2], unsigned *rate, + unsigned *channels) { + static const unsigned rates[] = {96000U, 88200U, 64000U, 48000U, 44100U, + 32000U, 24000U, 22050U, 16000U, 12000U, + 11025U, 8000U, 7350U}; + static const unsigned channel_counts[] = {0U, 1U, 2U, 3U, 4U, 5U, 6U, 8U}; + unsigned aot = (cfg[0] >> 3) & 0x1fU; + unsigned sfi = ((unsigned) (cfg[0] & 0x07U) << 1) | (cfg[1] >> 7); + unsigned cc = (cfg[1] >> 3) & 0x0fU; + + if (rate == NULL || channels == NULL) return false; + if (aot != 2U || sfi >= sizeof(rates) / sizeof(rates[0])) return false; + if (cc == 0U || cc >= sizeof(channel_counts) / sizeof(channel_counts[0])) { + return false; + } + + *rate = rates[sfi]; + *channels = channel_counts[cc]; + return *channels > 0; +} + +static uint64_t rtsp_aac_frame_period_ms(unsigned sample_rate) { + uint64_t period_ms; + + if (sample_rate == 0U) return 20U; + period_ms = (1024000ULL + sample_rate / 2U) / sample_rate; + return period_ms == 0 ? 1U : period_ms; +} + +static uint32_t rtsp_audio_rtp_timestamp(unsigned sample_rate, uint64_t pts_us) { + if (sample_rate == 0U) return 0U; + return (uint32_t) ((pts_us * sample_rate + 500000ULL) / 1000000ULL); +} + +static bool rtsp_build_sdp(struct rtsp_server *srv) { + char sps64[128], pps64[128], profile_level_id[7]; + char audio_cfg[128]; + struct mg_rtsp_frame frame; + struct rtsp_nal nals[RTSP_MAX_NALS]; + const uint8_t *sps = NULL, *pps = NULL; + struct rtsp_mp4_state *st = NULL, *audio_st = NULL; + size_t i, sps_len = 0, pps_len = 0, nals_count; + unsigned fps, audio_rate = 0, audio_channels = 0; + + srv->has_audio = srv->opts.audio_fn != NULL; + if (srv->has_audio && + !rtsp_aac_lc_config_parse(srv->opts.aac_lc_config, &audio_rate, + &audio_channels)) { + return false; + } + if (srv->opts.fn == mg_rtsp_get_mp4_frame && srv->opts.fn_data != NULL) { + st = rtsp_mp4_get_state((const char *) srv->opts.fn_data); + if (st == NULL || st->sps == NULL || st->pps == NULL) return false; + if (srv->opts.width == 0) srv->opts.width = st->width; + if (srv->opts.height == 0) srv->opts.height = st->height; + if (srv->opts.fps == 0) srv->opts.fps = st->fps; + sps = st->sps; + pps = st->pps; + sps_len = st->sps_len; + pps_len = st->pps_len; + } else { + if (srv->opts.fn == NULL) return false; + if (!srv->opts.fn(&frame, srv->opts.fn_data) || frame.data == NULL || + frame.len == 0) { + return false; + } + + nals_count = h264_split_annexb((const uint8_t *) frame.data, frame.len, nals, + sizeof(nals) / sizeof(nals[0])); + for (i = 0; i < nals_count; i++) { + if (nals[i].len == 0) continue; + switch (nals[i].ptr[0] & 0x1fU) { + case 7: + sps = nals[i].ptr; + sps_len = nals[i].len; + break; + case 8: + pps = nals[i].ptr; + pps_len = nals[i].len; + break; + default: + break; + } + } + if (sps == NULL || pps == NULL || sps_len < 4) return false; + } + + fps = srv->opts.fps == 0 ? 30U : srv->opts.fps; + if (srv->has_audio && srv->opts.audio_fn == mg_rtsp_get_mp4_audio && + srv->opts.audio_fn_data != NULL) { + audio_st = rtsp_mp4_get_state((const char *) srv->opts.audio_fn_data); + if (audio_st == NULL || audio_st->audio_samples == NULL || + audio_st->audio_sample_count == 0) { + return false; + } + } + + profile_level_id[0] = '\0'; + mg_snprintf(profile_level_id, sizeof(profile_level_id), "%02X%02X%02X", sps[1], + sps[2], sps[3]); + sps64[mg_base64_encode(sps, sps_len, sps64, sizeof(sps64) - 1U)] = '\0'; + pps64[mg_base64_encode(pps, pps_len, pps64, sizeof(pps64) - 1U)] = '\0'; + mg_snprintf( + srv->sdp, sizeof(srv->sdp), + "v=0\r\n" + "o=- 0 0 IN IP4 0.0.0.0\r\n" + "s=Mongoose RTSP Stream\r\n" + "t=0 0\r\n" + "a=control:*\r\n" + "m=video 0 RTP/AVP %u\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtpmap:%u H264/90000\r\n" + "a=fmtp:%u packetization-mode=1;profile-level-id=%s;" + "sprop-parameter-sets=%s,%s\r\n" + "a=framerate:%u\r\n" + "a=framesize:%u %u-%u\r\n" + "a=control:trackID=0\r\n", + RTSP_VIDEO_RTP_PT, RTSP_VIDEO_RTP_PT, RTSP_VIDEO_RTP_PT, profile_level_id, + sps64, pps64, fps, RTSP_VIDEO_RTP_PT, srv->opts.width, srv->opts.height); + if (srv->has_audio) { + rtsp_hex_encode(srv->opts.aac_lc_config, sizeof(srv->opts.aac_lc_config), + audio_cfg, sizeof(audio_cfg)); + mg_snprintf( + srv->sdp + strlen(srv->sdp), sizeof(srv->sdp) - strlen(srv->sdp), + "m=audio 0 RTP/AVP %u\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=rtpmap:%u MPEG4-GENERIC/%u/%u\r\n" + "a=fmtp:%u streamtype=5;profile-level-id=1;mode=AAC-hbr;" + "sizelength=13;indexlength=3;indexdeltalength=3;config=%s\r\n" + "a=control:trackID=1\r\n", + RTSP_AUDIO_RTP_PT, RTSP_AUDIO_RTP_PT, audio_rate, audio_channels, + RTSP_AUDIO_RTP_PT, audio_cfg); + } + return true; +} + +static void rtsp_send_rtp_packet(struct mg_connection *c, + struct rtsp_track_conn *tc, uint8_t pt, + bool marker, uint32_t timestamp, + const void *payload, size_t payload_len) { + uint8_t packet[4 + 12 + RTSP_RTP_PAYLOAD_MAX + 2]; + uint16_t packet_len = (uint16_t) (12U + payload_len); + uint16_t net16; + uint32_t net32; + + if (payload_len > sizeof(packet) - 16U) return; + packet[0] = '$'; + packet[1] = (uint8_t) tc->rtp_channel; + net16 = mg_htons(packet_len); + memcpy(packet + 2, &net16, sizeof(net16)); + packet[4] = 0x80U; + packet[5] = (uint8_t) ((marker ? 0x80U : 0U) | pt); + net16 = mg_htons(tc->rtp_seq++); + memcpy(packet + 6, &net16, sizeof(net16)); + net32 = mg_htonl(timestamp); + memcpy(packet + 8, &net32, sizeof(net32)); + net32 = mg_htonl(tc->ssrc); + memcpy(packet + 12, &net32, sizeof(net32)); + memcpy(packet + 16, payload, payload_len); + mg_send(c, packet, (size_t) packet_len + 4U); +} + +static void rtsp_send_h264_nal(struct mg_connection *c, + struct rtsp_track_conn *tc, const uint8_t *nal, + size_t nal_len, bool last_nal, + uint32_t timestamp) { + if (nal_len == 0) return; + if (nal_len <= RTSP_RTP_PAYLOAD_MAX) { + rtsp_send_rtp_packet(c, tc, RTSP_VIDEO_RTP_PT, last_nal, timestamp, nal, + nal_len); + } else { + size_t off = 1; + uint8_t fu[2 + RTSP_RTP_PAYLOAD_MAX]; + uint8_t nal_type = nal[0] & 0x1fU; + uint8_t nri = nal[0] & 0x60U; + size_t max_chunk = RTSP_RTP_PAYLOAD_MAX - 2U; + + fu[0] = (uint8_t) (nri | 28U); + while (off < nal_len) { + size_t chunk = nal_len - off; + bool first = off == 1U; + bool last = off + chunk >= nal_len; + if (chunk > max_chunk) chunk = max_chunk; + last = off + chunk >= nal_len; + fu[1] = (uint8_t) ((first ? 0x80U : 0U) | (last ? 0x40U : 0U) | nal_type); + memcpy(fu + 2, nal + off, chunk); + rtsp_send_rtp_packet(c, tc, RTSP_VIDEO_RTP_PT, last_nal && last, timestamp, + fu, chunk + 2U); + off += chunk; + } + } +} + +static void rtsp_send_access_unit(struct mg_connection *c, + struct rtsp_track_conn *tc, + const struct mg_rtsp_frame *frame) { + struct rtsp_nal nals[RTSP_MAX_NALS]; + size_t i, count; + uint32_t timestamp = + (uint32_t) ((frame->pts_us * RTSP_VIDEO_RTP_CLOCK) / 1000000ULL); + + count = h264_split_annexb((const uint8_t *) frame->data, frame->len, nals, + sizeof(nals) / sizeof(nals[0])); + for (i = 0; i < count; i++) { + rtsp_send_h264_nal(c, tc, nals[i].ptr, nals[i].len, i + 1U == count, + timestamp); + } +} + +static void rtsp_send_audio_sample(struct mg_connection *c, + struct rtsp_track_conn *tc, + const uint8_t *sample, size_t sample_len, + uint32_t timestamp) { + uint8_t payload[4 + RTSP_RTP_PAYLOAD_MAX]; + + if (sample == NULL || sample_len == 0 || sample_len + 4U > sizeof(payload)) return; + payload[0] = 0; + payload[1] = 16; + payload[2] = (uint8_t) (sample_len >> 5); + payload[3] = (uint8_t) ((sample_len & 0x1fU) << 3); + memcpy(payload + 4, sample, sample_len); + rtsp_send_rtp_packet(c, tc, RTSP_AUDIO_RTP_PT, true, timestamp, payload, + sample_len + 4U); +} + +static void rtsp_handle_options(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[192]; + (void) rc; + mg_snprintf(headers, sizeof(headers), + "Public: OPTIONS, DESCRIBE, SETUP, PLAY, PAUSE, TEARDOWN, " + "GET_PARAMETER\r\n"); + rtsp_send_status(c, req, 200, "OK", headers); +} + +static void rtsp_handle_describe(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[128]; + if (!rtsp_path_eq(req->uri, rc->srv->path)) { + rtsp_send_status(c, req, 404, "Not Found", NULL); + return; + } + mg_snprintf(headers, sizeof(headers), + "Content-Type: application/sdp\r\n" + "Content-Base: %.*s/\r\n", + (int) req->uri.len, req->uri.buf); + rtsp_send_response(c, req, 200, "OK", headers, rc->srv->sdp, + strlen(rc->srv->sdp)); +} + +static bool rtsp_any_setup_done(const struct rtsp_conn *rc) { + size_t i; + for (i = 0; i < RTSP_TRACK_COUNT; i++) { + if (rc->tracks[i].setup_done) return true; + } + return false; +} + +static void rtsp_handle_setup(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[160]; + int track_id = rtsp_track_path_eq(req->uri, rc->srv->path); + struct rtsp_track_conn *tc; + + if (track_id < 0 || (unsigned) track_id >= RTSP_TRACK_COUNT || + ((unsigned) track_id == RTSP_AUDIO_TRACK && !rc->srv->has_audio)) { + rtsp_send_status(c, req, 404, "Not Found", NULL); + return; + } + if (!rtsp_casencontains(req->transport, "RTP/AVP/TCP")) { + rtsp_send_status(c, req, 461, "Unsupported Transport", NULL); + return; + } + tc = &rc->tracks[track_id]; + rtsp_parse_interleaved_channels(req->transport, &tc->rtp_channel, &tc->rtcp_channel); + tc->setup_done = true; + tc->ended = false; + mg_snprintf(headers, sizeof(headers), + "Transport: RTP/AVP/TCP;unicast;interleaved=%u-%u\r\n" + "Session: %s\r\n", + tc->rtp_channel, tc->rtcp_channel, rc->session_id); + rtsp_send_status(c, req, 200, "OK", headers); +} + +static void rtsp_handle_play(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[128]; + size_t i; + uint64_t now_ms; + + if (!rtsp_any_setup_done(rc)) { + rtsp_send_status(c, req, 455, "Method Not Valid In This State", NULL); + return; + } + if (!rtsp_session_matches(rc, req->session)) { + rtsp_send_status(c, req, 454, "Session Not Found", NULL); + return; + } + rc->playing = true; + now_ms = mg_millis(); + for (i = 0; i < RTSP_TRACK_COUNT; i++) { + if (rc->tracks[i].setup_done) { + rc->tracks[i].deadline_ms = now_ms; + rc->tracks[i].ended = false; + } + } + if (rc->srv->opts.fn == mg_rtsp_get_mp4_frame && rc->srv->opts.fn_data != NULL) { + rtsp_mp4_reset((const char *) rc->srv->opts.fn_data); + } + if (rc->srv->opts.audio_fn == mg_rtsp_get_mp4_audio && + rc->srv->opts.audio_fn_data != NULL) { + rtsp_mp4_reset((const char *) rc->srv->opts.audio_fn_data); + } + mg_snprintf(headers, sizeof(headers), "Session: %s\r\nRange: npt=0.000-\r\n", + rc->session_id); + rtsp_send_status(c, req, 200, "OK", headers); +} + +static void rtsp_handle_pause(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[64]; + if (!rtsp_session_matches(rc, req->session)) { + rtsp_send_status(c, req, 454, "Session Not Found", NULL); + return; + } + rc->playing = false; + mg_snprintf(headers, sizeof(headers), "Session: %s\r\n", rc->session_id); + rtsp_send_status(c, req, 200, "OK", headers); +} + +static void rtsp_handle_teardown(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[64]; + if (!rtsp_session_matches(rc, req->session)) { + rtsp_send_status(c, req, 454, "Session Not Found", NULL); + return; + } + rc->playing = false; + mg_snprintf(headers, sizeof(headers), "Session: %s\r\n", rc->session_id); + rtsp_send_status(c, req, 200, "OK", headers); + c->is_draining = 1; +} + +static void rtsp_handle_get_parameter(struct mg_connection *c, + struct rtsp_conn *rc, + const struct rtsp_request *req) { + char headers[64]; + (void) rc; + mg_snprintf(headers, sizeof(headers), "Session: %s\r\n", rc->session_id); + rtsp_send_status(c, req, 200, "OK", headers); +} + +static void rtsp_dispatch_request(struct mg_connection *c, struct rtsp_conn *rc, + const struct rtsp_request *req) { + if (rtsp_caseeq(req->method, "OPTIONS")) { + rtsp_handle_options(c, rc, req); + } else if (rtsp_caseeq(req->method, "DESCRIBE")) { + rtsp_handle_describe(c, rc, req); + } else if (rtsp_caseeq(req->method, "SETUP")) { + rtsp_handle_setup(c, rc, req); + } else if (rtsp_caseeq(req->method, "PLAY")) { + rtsp_handle_play(c, rc, req); + } else if (rtsp_caseeq(req->method, "PAUSE")) { + rtsp_handle_pause(c, rc, req); + } else if (rtsp_caseeq(req->method, "TEARDOWN")) { + rtsp_handle_teardown(c, rc, req); + } else if (rtsp_caseeq(req->method, "GET_PARAMETER")) { + rtsp_handle_get_parameter(c, rc, req); + } else { + rtsp_send_status(c, req, 405, "Method Not Allowed", NULL); + } +} + +static void rtsp_handle_read(struct mg_connection *c) { + struct rtsp_conn *rc = (struct rtsp_conn *) c->fn_data; + + for (;;) { + if (c->recv.len >= 4 && ((uint8_t *) c->recv.buf)[0] == '$') { + size_t frame_len = + ((size_t) ((uint8_t *) c->recv.buf)[2] << 8) | ((uint8_t *) c->recv.buf)[3]; + if (c->recv.len < frame_len + 4U) return; + mg_iobuf_del(&c->recv, 0, frame_len + 4U); + continue; + } + + { + struct rtsp_request req; + int result = rtsp_parse_request((const char *) c->recv.buf, c->recv.len, &req); + if (result == 0) return; + if (result < 0) { + c->is_closing = 1; + return; + } + rtsp_dispatch_request(c, rc, &req); + mg_iobuf_del(&c->recv, 0, req.msg_len); + } + } +} + +static void rtsp_handle_poll(struct mg_connection *c, uint64_t now_ms) { + struct rtsp_conn *rc = (struct rtsp_conn *) c->fn_data; + struct rtsp_mp4_state *video_st = NULL, *audio_st = NULL; + unsigned audio_rate = 0, audio_channels = 0; + unsigned fps = rc->srv->opts.fps == 0 ? 30U : rc->srv->opts.fps; + uint64_t video_fallback_ms = 1000U / fps; + uint64_t audio_fallback_ms = 20U; + unsigned burst = 0, max_burst = 128U; + size_t send_limit = 256U * 1024U; + + if (!rc->playing) return; + if (!rc->tracks[RTSP_VIDEO_TRACK].setup_done && + !rc->tracks[RTSP_AUDIO_TRACK].setup_done) { + return; + } + if (video_fallback_ms == 0) video_fallback_ms = 1; + if (rc->srv->opts.fn == NULL) return; + if (c->send.len >= send_limit) return; + + if (rc->srv->opts.fn == mg_rtsp_get_mp4_frame && rc->srv->opts.fn_data != NULL) { + video_st = rtsp_mp4_get_state((const char *) rc->srv->opts.fn_data); + if (video_st == NULL) { + rc->playing = false; + c->is_draining = 1; + return; + } + } + if (rc->srv->has_audio) { + if (!rtsp_aac_lc_config_parse(rc->srv->opts.aac_lc_config, &audio_rate, + &audio_channels)) { + rc->playing = false; + c->is_draining = 1; + return; + } + (void) audio_channels; + audio_fallback_ms = rtsp_aac_frame_period_ms(audio_rate); + if (rc->srv->opts.audio_fn == mg_rtsp_get_mp4_audio && + rc->srv->opts.audio_fn_data != NULL) { + audio_st = rtsp_mp4_get_state((const char *) rc->srv->opts.audio_fn_data); + if (audio_st == NULL) { + rc->playing = false; + c->is_draining = 1; + return; + } + } + } + + if (rc->tracks[RTSP_VIDEO_TRACK].setup_done && video_st != NULL && + video_st->sample_index >= video_st->sample_count) { + rc->tracks[RTSP_VIDEO_TRACK].ended = true; + } + if (rc->tracks[RTSP_AUDIO_TRACK].setup_done && audio_st != NULL && + (audio_st->audio_samples == NULL || + audio_st->audio_sample_index >= audio_st->audio_sample_count)) { + rc->tracks[RTSP_AUDIO_TRACK].ended = true; + } + + while (rc->tracks[RTSP_VIDEO_TRACK].setup_done && + !rc->tracks[RTSP_VIDEO_TRACK].ended && c->send.len < send_limit && + mg_timer_expired(&rc->tracks[RTSP_VIDEO_TRACK].deadline_ms, + video_st != NULL + ? rtsp_mp4_track_period_ms( + video_st->samples, video_st->sample_count, + video_st->sample_index, video_fallback_ms) + : video_fallback_ms, + now_ms)) { + struct mg_rtsp_frame frame; + if (!rc->srv->opts.fn(&frame, rc->srv->opts.fn_data) || frame.data == NULL || + frame.len == 0) { + rc->tracks[RTSP_VIDEO_TRACK].ended = true; + break; + } + rtsp_send_access_unit(c, &rc->tracks[RTSP_VIDEO_TRACK], &frame); + if (++burst >= max_burst) break; + } + + while (rc->srv->has_audio && rc->tracks[RTSP_AUDIO_TRACK].setup_done && + !rc->tracks[RTSP_AUDIO_TRACK].ended && c->send.len < send_limit && + mg_timer_expired(&rc->tracks[RTSP_AUDIO_TRACK].deadline_ms, + audio_st != NULL + ? rtsp_mp4_track_period_ms( + audio_st->audio_samples, + audio_st->audio_sample_count, + audio_st->audio_sample_index, + audio_fallback_ms) + : audio_fallback_ms, + now_ms)) { + struct mg_rtsp_audio audio; + if (rc->srv->opts.audio_fn == NULL || + !rc->srv->opts.audio_fn(&audio, rc->srv->opts.audio_fn_data) || + audio.data == NULL || audio.len == 0) { + rc->tracks[RTSP_AUDIO_TRACK].ended = true; + break; + } + rtsp_send_audio_sample(c, &rc->tracks[RTSP_AUDIO_TRACK], audio.data, audio.len, + rtsp_audio_rtp_timestamp(audio_rate, audio.pts_us)); + if (++burst >= max_burst) break; + } + + if ((!rc->tracks[RTSP_VIDEO_TRACK].setup_done || + rc->tracks[RTSP_VIDEO_TRACK].ended) && + (!rc->tracks[RTSP_AUDIO_TRACK].setup_done || + rc->tracks[RTSP_AUDIO_TRACK].ended)) { + rc->playing = false; + c->is_draining = 1; + } +} + +static void rtsp_free_server(struct rtsp_server *srv) { + if (srv == NULL) return; + mg_free(srv->path); + mg_free(srv); +} + +static void rtsp_fn(struct mg_connection *c, int ev, void *ev_data) { + if (c->is_listening) { + if (ev == MG_EV_CLOSE) rtsp_free_server((struct rtsp_server *) c->fn_data); + return; + } + + switch (ev) { + case MG_EV_ACCEPT: { + struct rtsp_server *srv = (struct rtsp_server *) c->fn_data; + struct rtsp_conn *rc = + (struct rtsp_conn *) mg_calloc(1, sizeof(struct rtsp_conn)); + if (rc == NULL) { + c->is_closing = 1; + break; + } + rc->srv = srv; + rc->tracks[RTSP_VIDEO_TRACK].rtp_channel = 0; + rc->tracks[RTSP_VIDEO_TRACK].rtcp_channel = 1; + rc->tracks[RTSP_VIDEO_TRACK].rtp_seq = (uint16_t) c->id; + rc->tracks[RTSP_VIDEO_TRACK].ssrc = 0x13572468U ^ (uint32_t) c->id; + rc->tracks[RTSP_AUDIO_TRACK].rtp_channel = 2; + rc->tracks[RTSP_AUDIO_TRACK].rtcp_channel = 3; + rc->tracks[RTSP_AUDIO_TRACK].rtp_seq = (uint16_t) (c->id ^ 0x4000U); + rc->tracks[RTSP_AUDIO_TRACK].ssrc = 0x24681357U ^ (uint32_t) c->id; + mg_snprintf(rc->session_id, sizeof(rc->session_id), "%lu", c->id); + c->fn_data = rc; + break; + } + case MG_EV_READ: + rtsp_handle_read(c); + break; + case MG_EV_POLL: + rtsp_handle_poll(c, *(uint64_t *) ev_data); + break; + case MG_EV_CLOSE: + mg_free(c->fn_data); + break; + default: + break; + } +} + +struct mg_connection *mg_rtsp_listen(struct mg_mgr *mgr, const char *url, + const struct mg_rtsp_opts *opts) { + struct mg_connection *c; + struct rtsp_server *srv; + if (mgr == NULL || url == NULL || opts == NULL || opts->fn == NULL) return NULL; + + srv = (struct rtsp_server *) mg_calloc(1, sizeof(*srv)); + if (srv == NULL) return NULL; + srv->opts = *opts; + srv->path = rtsp_strdup(mg_url_uri(url)); + if (srv->path == NULL || !rtsp_build_sdp(srv)) { + rtsp_free_server(srv); + return NULL; + } + + c = mg_listen(mgr, url, rtsp_fn, srv); + if (c == NULL) { + rtsp_free_server(srv); + return NULL; + } + return c; +} diff --git a/tutorials/tcp/rtsp-video-stream/rtsp.h b/tutorials/tcp/rtsp-video-stream/rtsp.h new file mode 100644 index 00000000..948798d3 --- /dev/null +++ b/tutorials/tcp/rtsp-video-stream/rtsp.h @@ -0,0 +1,54 @@ +#pragma once +#include "mongoose.h" + +// Encoded video frame provided by user code +struct mg_rtsp_frame { + const void *data; // H.264 encoded frame (one access unit) + size_t len; // Frame size in bytes + uint64_t pts_us; // Presentation timestamp, microseconds + bool keyframe; // true for IDR frame +}; + +// Encoded audio frame provided by user code +struct mg_rtsp_audio { + const void *data; // AAC encoded frame, one access unit, no ADTS header + size_t len; // Frame size in bytes + uint64_t pts_us; // Presentation timestamp, microseconds +}; + +// Called by Mongoose when it needs the next video frame +// - frame->data must stay valid until the callback returns +// - Mongoose copies/packetizes it before returning +typedef bool (*mg_rtsp_next_frame_fn)(struct mg_rtsp_frame *frame, + void *fn_data); + +// Called by Mongoose when it needs the next audio frame +// - audio->data must stay valid until the callback returns +// - Mongoose copies/packetizes it before returning +typedef bool (*mg_rtsp_next_audio_fn)(struct mg_rtsp_audio *audio, + void *fn_data); + +// RTSP listener options (H.264 video, optional AAC audio) +struct mg_rtsp_opts { + unsigned width; // Optional, 0 if unknown + unsigned height; // Optional, 0 if unknown + unsigned fps; // Optional, 0 if unknown/variable + + mg_rtsp_next_frame_fn fn; // User callback to provide frames + void *fn_data; // User callback data + + mg_rtsp_next_audio_fn audio_fn; // Optional AAC callback, NULL if no audio + void *audio_fn_data; // Audio callback data + uint8_t aac_lc_config[2]; // AAC-LC config, e.g. {0x12, 0x10}; + // sample rate and channels are inferred + // e.g. {0x12, 0x10} is 44.1 kHz, stereo. +}; +// Start RTSP server listener +// E.g. mg_rtsp_listen(&mgr, "rtsp://0.0.0.0:8554/cam", &opts); +struct mg_connection *mg_rtsp_listen(struct mg_mgr *mgr, const char *url, + const struct mg_rtsp_opts *opts); + +// Helper callbacks that get H.264 video audio from the MP4 file. +// fn_data must be a const char * path to the MP4 file. E.g. "test.mp4" +bool mg_rtsp_get_mp4_frame(struct mg_rtsp_frame *frame, void *fn_data); +bool mg_rtsp_get_mp4_audio(struct mg_rtsp_audio *audio, void *fn_data); diff --git a/tutorials/tcp/rtsp-video-stream/test.mp4 b/tutorials/tcp/rtsp-video-stream/test.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..bbe52d88cd6a6982bd579e5a3f9afd69812e4729 GIT binary patch literal 20507 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 JAr2iu{68pA;U541 literal 0 HcmV?d00001