Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.24)

project(moq2ts-ui
VERSION 0.1.0
VERSION 1.1.0
DESCRIPTION "Cross-platform MOQ live publisher for H.264/AAC-Opus M2TS"
LANGUAGES C CXX)

Expand Down
36 changes: 29 additions & 7 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ ffmpeg -hide_banner -re \

`-g 50` at 25 fps gives a random access point every 2 s. MSFTS-02 sets no
maximum Group duration. To check received source packets against the sent feed,
see the end-to-end test in [tests/srt-mpts-e2e.sh](../tests/srt-mpts-e2e.sh).
see "End-to-end tests" below.

A multi-program feed is published as one `unmodified-multiplex` track:

Expand Down Expand Up @@ -365,18 +365,40 @@ The once-per-second readout includes checks corresponding to
[ETSI TR 101 290 section 5.2](https://www.etsi.org/deliver/etsi_tr/101200_101299/101290/01.04.01_60/tr_101290v010401p.pdf);
this is a subset of the standard's full monitoring checks.

For a two-program SRT relay check with byte-level source comparison:
### End-to-end tests

Three scripts in `tests/` run the real publisher and receiver through a relay.
They are not part of `ctest` or CI, because they need a real build, an installed
moq5, and a draft-22 relay:

| Script | Checks |
| --- | --- |
| `srt-mpts-e2e.sh` | Two-program SRT feed, published as one multiplex track; the capture is byte-identical to the source |
| `mpts-per-program-e2e.sh` | `--config` with an SRT listener; each program arrives as its own track with only its PIDs and no continuity error |
| `mpts-end-broadcast-e2e.sh` | `--config` with a FIFO source that ends; every receiver sees the broadcast end (the closing catalog of MSF -01 Section 11.3, `[rx] track ended`) long before its own `--duration` |

By default a script starts a local `moq5-relay` (draft 22, loopback, a
throwaway certificate), so a run publishes to no public service. Point
`MOQ5_RELAY` at the binary, built from moq5 main with the MsQuic managed
facade (`MOQ_BUILD_ADAPTER_MSQUIC=ON`, `MOQ_BUILD_MSQUIC_MANAGED=ON`).
`MOQ5_RELAY_LD_LIBRARY_PATH` adds a library path the relay needs, such as the
MsQuic build directory:

```bash
RELAY_URL=moqt://127.0.0.1:4433 \
MOQ5_RELAY=/path/to/moq5-relay \
MOQ2TS_CLI=./build/moq2ts-cli \
MOQ2TS_RECEIVER=./build-receiver/moq2ts-receiver \
tests/srt-mpts-e2e.sh
tests/mpts-end-broadcast-e2e.sh
```

The script reports any MOQT gaps; it requires real media, source-identical
packets, and two decodable program entries. It leaves its capture and logs in
the printed temporary directory.
Set `RELAY_URL` to use an existing relay instead, for example
`RELAY_URL=moqt://127.0.0.1:4433`; the local relay is then not started. The
receiver needs moq5 built with the `service` and `mpeg2ts` targets, and moq5 with
`MOQ_BUILD_PQ_THREADED=ON` for `moqt://` endpoints.

Each script reports any MOQT gaps, requires real media, and leaves its capture
and logs in the printed temporary directory when it fails. `srt-mpts-e2e.sh`
also requires source-identical packets and two decodable program entries.

## Verify byte-faithful passthrough

Expand Down
1 change: 1 addition & 0 deletions src/receive/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ int main(int argc, char **argv) {
/* A catalog that drops the track, such as the isComplete
* catalog that ends a live broadcast (MSF Section 11.3),
* ends it too. */
fputs("[rx] track ended\n", stderr);
ended = true;
} else if ((ev.kind == MOQ_MEDIA_TRACK_LOCAL_FAILURE ||
ev.kind == MOQ_MEDIA_TRACK_DELIVERY_LOCAL_FAILURE) &&
Expand Down
53 changes: 53 additions & 0 deletions tests/e2e-relay.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Sourced by the end-to-end scripts. e2e_relay_start <work-dir> sets RELAY_URL.
#
# Without RELAY_URL it starts a local moq5-relay (draft 22) on loopback with a
# throwaway certificate, so a run publishes to no public service. It stops with
# the script when the script's cleanup kills "${pids[@]}". With RELAY_URL set,
# that relay is used as is.
#
# Environment:
# RELAY_URL an existing relay, e.g. moqt://127.0.0.1:4433;
# skips the local relay
# MOQ5_RELAY the moq5-relay binary (needed without RELAY_URL)
# MOQ5_RELAY_LD_LIBRARY_PATH library path the relay needs, e.g. the MsQuic
# build directory (optional)
# RELAY_PORT UDP port of the local relay (default: a free one)

e2e_relay_start() {
local work="$1"
if [ -n "${RELAY_URL:-}" ]; then
echo "using relay $RELAY_URL"
return 0
fi
: "${MOQ5_RELAY:?set RELAY_URL to a draft-22 relay, or MOQ5_RELAY to the moq5-relay binary to start a local one}"
command -v openssl > /dev/null || { echo "missing required tool: openssl" >&2; return 1; }
local port="${RELAY_PORT:-}"
if [ -z "$port" ]; then
port="$(python3 -c 'import socket; s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1])')"
fi
local dir="$work/relay"
mkdir -p "$dir"
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout "$dir/key.pem" -out "$dir/cert.pem" -days 2 -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2> /dev/null
cat > "$dir/relay.json" <<JSON
{
"listener": { "host": "127.0.0.1", "port": $port, "versions": [22], "lanes": 1,
"cert": "$dir/cert.pem", "key": "$dir/key.pem" },
"auth": { "mode": "allow_all" }
}
JSON
LD_LIBRARY_PATH="${MOQ5_RELAY_LD_LIBRARY_PATH:+$MOQ5_RELAY_LD_LIBRARY_PATH:}${LD_LIBRARY_PATH:-}" \
"$MOQ5_RELAY" serve --config "$dir/relay.json" > "$dir/relay.log" 2>&1 &
local relay=$!
pids+=("$relay")
local i
for i in $(seq 1 50); do
grep -q "listening on 127.0.0.1:$port" "$dir/relay.log" 2> /dev/null && break
kill -0 "$relay" 2> /dev/null || { echo "the local relay exited at start-up:" >&2; tail -n 8 "$dir/relay.log" >&2; return 1; }
sleep 0.2
done
grep -q "listening on 127.0.0.1:$port" "$dir/relay.log" || { echo "the local relay did not start" >&2; return 1; }
RELAY_URL="moqt://127.0.0.1:$port"
echo "started a local relay on $RELAY_URL"
}
127 changes: 127 additions & 0 deletions tests/mpts-end-broadcast-e2e.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/env bash
# A two-program FIFO source that ends -> moq2ts-cli --config -> relay -> one
# moq2ts-receiver per program. When the source ends cleanly the publisher ends
# the broadcast with the closing catalog (MSF -01 Section 11.3), and moqxr
# ends every track before it. Each receiver must report "[rx] track ended" long
# before its own --duration runs out, with every program's packets intact.
# Exits non-zero otherwise.
# Requires a draft-22 relay (a local moq5-relay by default, see e2e-relay.sh),
# ffmpeg, and python3.
#
# Environment:
# RELAY_URL an existing relay, e.g. moqt://127.0.0.1:4433; without it
# a local relay starts from MOQ5_RELAY (see e2e-relay.sh)
# MOQ2TS_CLI the moq2ts-cli binary (required)
# MOQ2TS_RECEIVER the moq2ts-receiver binary (required)
# SOURCE_SECONDS length of the source in seconds (default 15)
# RX_DURATION upper bound of each receiver, in seconds (default 90)
# NAMESPACE MOQT namespace (default e2e/mpts-end/<time>)
set -euo pipefail

: "${MOQ2TS_CLI:?set MOQ2TS_CLI to the moq2ts-cli binary}"
: "${MOQ2TS_RECEIVER:?set MOQ2TS_RECEIVER to the moq2ts-receiver binary}"
SOURCE_SECONDS="${SOURCE_SECONDS:-15}"
RX_DURATION="${RX_DURATION:-90}"
NAMESPACE="${NAMESPACE:-e2e/mpts-end/$(date +%s)}"
PROGRAMS=(101 102)
MUX_RATE=3000000
# The receivers start about 4 s after the source and must end within this many
# seconds of it: the source length plus a margin, far below RX_DURATION.
END_WITHIN=$((SOURCE_SECONDS + 25))
for tool in ffmpeg python3 timeout mkfifo; do
command -v "$tool" > /dev/null || { echo "missing required tool: $tool" >&2; exit 1; }
done

work="$(mktemp -d)"
pids=()
ok=0
cleanup() {
for pid in "${pids[@]}"; do kill "$pid" 2>/dev/null || true; done
wait 2>/dev/null || true
if [ "$ok" -eq 1 ]; then rm -rf "$work"; else echo "artifacts kept in $work" >&2; fi
}
trap cleanup EXIT
# shellcheck source=tests/e2e-relay.sh
. "$(dirname "${BASH_SOURCE[0]}")/e2e-relay.sh"
e2e_relay_start "$work" || exit 1
fail() {
echo "FAIL: $*" >&2
for log in publisher sender; do
echo "--- last lines of $log.log" >&2
tail -n 8 "$work/$log.log" >&2 || true
done
exit 1
}

# Two programs, each with its own video and audio, in one CBR multiplex.
timeout 120 ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i testsrc2=size=320x180:rate=25 -f lavfi -i sine=frequency=1000 \
-f lavfi -i testsrc2=size=320x180:rate=25 -f lavfi -i sine=frequency=500 \
-map 0 -map 1 -map 2 -map 3 -c:v libx264 -preset ultrafast -g 25 -bf 0 -b:v 600k \
-c:a aac -b:a 64k \
-program program_num=101:title=A:st=0:st=1 -program program_num=102:title=B:st=2:st=3 \
-muxrate "$MUX_RATE" -t "$((SOURCE_SECONDS + 10))" -f mpegts "$work/source.ts"

mkfifo "$work/live.fifo"
cat > "$work/mpts.json" <<JSON
{
"relay": { "endpoint": "$RELAY_URL", "namespace": "$NAMESPACE", "draft": 22 },
"ingest": { "path": "$work/live.fifo" },
"programs": [ { "number": ${PROGRAMS[0]} }, { "number": ${PROGRAMS[1]} } ]
}
JSON

"$MOQ2TS_CLI" --config "$work/mpts.json" > "$work/publisher.log" 2>&1 &
pids+=($!)
publisher=$!
# The source ends after SOURCE_SECONDS: ffmpeg exits and closes the FIFO, which
# is a clean end of the source for the publisher.
ffmpeg -hide_banner -loglevel error -y -re -i "$work/source.ts" -t "$SOURCE_SECONDS" -map 0 -c copy \
-program program_num=101:title=A:st=0:st=1 -program program_num=102:title=B:st=2:st=3 \
-muxrate "$MUX_RATE" -f mpegts "$work/live.fifo" > "$work/sender.log" 2>&1 &
pids+=($!)
sender=$!
sleep 4
kill -0 "$publisher" 2>/dev/null || fail "the publisher exited before the receivers started"
kill -0 "$sender" 2>/dev/null || fail "the sender exited before the receivers started"

started=$SECONDS
rx=()
for n in "${PROGRAMS[@]}"; do
timeout "$((RX_DURATION + 30))" "$MOQ2TS_RECEIVER" --url "$RELAY_URL" --namespace "$NAMESPACE" --draft 22 \
--track "program-$n" --duration "$RX_DURATION" --insecure --out "$work/program-$n.ts" \
--stats-json "$work/program-$n.json" > "$work/receiver-$n.out" 2> "$work/receiver-$n.log" &
rx+=($!)
pids+=($!)
done
status=0
for pid in "${rx[@]}"; do wait "$pid" || status=1; done
elapsed=$((SECONDS - started))
[ "$status" -eq 0 ] || { tail -n 5 "$work"/receiver-*.log >&2; fail "a receiver failed or timed out"; }

for n in "${PROGRAMS[@]}"; do
grep -q '^\[rx\] track ended' "$work/receiver-$n.log" ||
fail "program $n: the receiver did not see the broadcast end (ran $elapsed s of $RX_DURATION s)"
done
[ "$elapsed" -le "$END_WITHIN" ] ||
fail "the receivers ended after $elapsed s, want at most $END_WITHIN s for a $SOURCE_SECONDS s source"

timeout 60 python3 - "$work" "${PROGRAMS[@]}" <<'PY' || fail "a capture is not a clean program"
import json
import pathlib
import sys

work = pathlib.Path(sys.argv[1])
for program in (int(p) for p in sys.argv[2:]):
data = (work / f"program-{program}.ts").read_bytes()
stats = json.loads((work / f"program-{program}.json").read_text())
assert len(data) >= 188 * 100 and len(data) % 188 == 0, f"program {program}: {len(data)} bytes"
assert stats["bytes"] == len(data) and stats["objects"] > 0, stats
assert stats["sync_errors"] == 0 and stats["malformed_packets"] == 0, stats
assert stats["transport_errors"] == 0 and stats["continuity_errors"] == 0, stats
print(f"program {program}: ended the broadcast cleanly after {stats['objects']} objects, "
f"{len(data) // 188} packets, continuity errors 0")
PY

echo "both receivers saw the broadcast end after $elapsed s (source $SOURCE_SECONDS s, receiver limit $RX_DURATION s)"
ok=1
10 changes: 7 additions & 3 deletions tests/mpts-per-program-e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
# Each program is published as the per-program track program-<n> (MSFTS 5.3); the
# script checks that each capture holds only its program's PIDs plus the PAT and PMT,
# every PID of the program, and no continuity error. Exits non-zero otherwise.
# Requires a running draft-22 relay, ffmpeg, ffprobe, and python3.
# Requires a draft-22 relay (a local moq5-relay by default, see e2e-relay.sh),
# ffmpeg, ffprobe, and python3.
#
# Environment:
# RELAY_URL relay, e.g. moqt://127.0.0.1:4433 (required)
# RELAY_URL an existing relay, e.g. moqt://127.0.0.1:4433; without it
# a local relay starts from MOQ5_RELAY (see e2e-relay.sh)
# MOQ2TS_CLI the moq2ts-cli binary (required)
# MOQ2TS_RECEIVER the moq2ts-receiver binary (required)
# SRT_PORT SRT listener port of moq2ts-cli (default 9124)
Expand All @@ -17,7 +19,6 @@
# NAMESPACE MOQT namespace (default e2e/mpts/<time>)
set -euo pipefail

: "${RELAY_URL:?set RELAY_URL to the relay, e.g. moqt://127.0.0.1:4433}"
: "${MOQ2TS_CLI:?set MOQ2TS_CLI to the moq2ts-cli binary}"
: "${MOQ2TS_RECEIVER:?set MOQ2TS_RECEIVER to the moq2ts-receiver binary}"
SRT_PORT="${SRT_PORT:-9124}"
Expand All @@ -43,6 +44,9 @@ cleanup() {
if [ "$ok" -eq 1 ]; then rm -rf "$work"; else echo "artifacts kept in $work" >&2; fi
}
trap cleanup EXIT
# shellcheck source=tests/e2e-relay.sh
. "$(dirname "${BASH_SOURCE[0]}")/e2e-relay.sh"
e2e_relay_start "$work" || exit 1
# Print the tail of the publisher and sender logs, then fail.
fail() {
echo "FAIL: $*" >&2
Expand Down
7 changes: 5 additions & 2 deletions tests/srt-mpts-e2e.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# Two-program SRT -> moq2ts publisher -> draft-22 relay -> moq5 receiver.
# Requires a running relay and installed ffmpeg/ffprobe.
# Requires a draft-22 relay (a local moq5-relay by default, see e2e-relay.sh)
# and installed ffmpeg/ffprobe. RELAY_URL selects an existing relay instead.
set -euo pipefail

: "${RELAY_URL:?set RELAY_URL to the relay, e.g. moqt://127.0.0.1:4433}"
: "${MOQ2TS_CLI:?set MOQ2TS_CLI to the moq2ts-cli binary}"
: "${MOQ2TS_RECEIVER:?set MOQ2TS_RECEIVER to the moq2ts-receiver binary}"
SRT_PORT="${SRT_PORT:-9123}"
Expand All @@ -16,6 +16,9 @@ cleanup() {
wait 2>/dev/null || true
}
trap cleanup EXIT
# shellcheck source=tests/e2e-relay.sh
. "$(dirname "${BASH_SOURCE[0]}")/e2e-relay.sh"
e2e_relay_start "$work" || exit 1

ffmpeg -hide_banner -loglevel error -y \
-f lavfi -i testsrc2=size=320x180:rate=25 -f lavfi -i sine=frequency=1000 \
Expand Down
Loading