changeset 0:05ff0f7ac977

initial commit: split from gsm-codec-lib
author Mychaela Falconia <falcon@freecalypso.org>
date Sun, 02 Apr 2023 00:28:28 +0000
parents
children 41eba040785a
files .hgignore Makefile README doc/RTP-analysis rtp-cont-check.c rtp-g711-extr.c rtp-gsmfr-extr.c rtp-jitter-view.c
diffstat 8 files changed, 965 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,8 @@
+syntax: regexp
+
+\.[oa]$
+
+^rtp-cont-check$
+^rtp-g711-extr$
+^rtp-gsmfr-extr$
+^rtp-jitter-view$
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,25 @@
+CC=	gcc
+CFLAGS=	-O2
+PROGS=	rtp-cont-check rtp-g711-extr rtp-gsmfr-extr rtp-jitter-view
+INSTBIN=/opt/freecalypso/bin
+
+all:	${PROGS}
+
+rtp-cont-check:	rtp-cont-check.c
+	${CC} ${CFLAGS} -o $@ $@.c -lpcap
+
+rtp-g711-extr:	rtp-g711-extr.c
+	${CC} ${CFLAGS} -o $@ $@.c -lpcap
+
+rtp-gsmfr-extr:	rtp-gsmfr-extr.c
+	${CC} ${CFLAGS} -o $@ $@.c -lpcap
+
+rtp-jitter-view:	rtp-jitter-view.c
+	${CC} ${CFLAGS} -o $@ $@.c -lpcap
+
+install:
+	mkdir -p ${INSTBIN}
+	install -c ${PROGS} ${INSTBIN}
+
+clean:
+	rm -f *.o *.out ${PROGS}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,6 @@
+This repository contains RTP analysis utilities (currently pcap-based, possibly
+others in the future) that have been developed at FreeCalypso and Themyscira
+Wireless HQ.  These utilities have been split from Themyscira Wireless GSM codec
+libraries and utilities package; this split was made so that we can work on our
+RTP debug tools independently, while our GSM codec libraries are under tight
+release process control.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/RTP-analysis	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,47 @@
+The present package includes a number of utilities for analyzing RTP streams
+that have been captured with tcpdump or equivalent tools in pcap format.  In
+order to use any of these utilities, you need to have a pcap file (obviously),
+and you need to identify the RTP stream to be analyzed or extracted by either
+source or destination IP:port.  All tools begin by applying a filter,
+considering only those packets that are UDP in IPv4 (no IPv6 support currently),
+and only those that match the specified source or destination IP:port.  Every
+matched packet is checked for a valid RTP header, and then the actual RTP stream
+analysis or extraction takes place, depending on the specific tool:
+
+rtp-cont-check	This program checks the selected RTP stream for continuity.  It
+		verifies that every matched packet has the same SSRC, that the
+		sequence number always increments by 1 from each individual
+		packet to the next, and that the RTP header timestamp always
+		increments by 160 units.  (The assumption is that the
+		application at hand is in the traditional telephony domain,
+		with a sampling rate of 8000 samples/s and 20 ms packetization
+		for RTP.)  This tool also looks at the capture time deltas
+		between successive packets and reports the observed minimum and
+		maximum; by seeing min and max delta-T, a developer can easily
+		notice timing aberrations that aren't caught by RTP header
+		sequence number and timestamp checks.
+
+rtp-g711-extr	This program focuses on a single selected RTP stream like the
+		others, enforces its continuity just like rtp-cont-check, and
+		then further enforces that every RTP packet be a 160-byte
+		payload, presumed to be either PCMU or PCMA.  (The payload type
+		number is NOT considered, only the payload length.)  The
+		selected G.711 RTP stream is then extracted and written into a
+		raw binary file.
+
+rtp-gsmfr-extr	This program focuses on a single selected RTP stream like the
+		others, enforces its continuity just like rtp-cont-check, but
+		then further enforces that every RTP packet be one of these 3
+		possibilities: a GSM FR1 codec frame, a GSM EFR codec frame or
+		a Themyscira BFI marker as defined in the RTP-BFI-extension
+		document.  (The payload type number is NOT considered, only the
+		payload length and the characteristic signature of each of the
+		3 allowed possibilities.)  The selected RTP stream is then
+		extracted and written into a gsmx file (see Binary-file-format),
+		which can then be analyzed with gsmrec-dump or decoded to
+		playable WAV with gsmfr-decode or gsmefr-decode.
+
+rtp-jitter-view	This program analyzes a single selected RTP stream just like
+		rtp-cont-check, but instead of reporting minimum and maximum
+		time deltas for the entire stream, it prints the individual
+		capture time delta between every successive pair of packets.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rtp-cont-check.c	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,234 @@
+/*
+ * This program reads a pcap file, extracts packets belonging to a
+ * particular RTP stream as identified by a source or destination
+ * IP:port, and checks its continuity: verifies that the sequence
+ * number always increments by 1 and that the timestamp always
+ * increments by 160 units.  Finally, this program prints out
+ * the minimum and maximum observed capture time deltas between
+ * successive packets of the stream.
+ *
+ * The codec can be anything, and this program can also be used to
+ * check continuity of RTP streams coming from PSTN/SIP, but the
+ * core assumption is that packets must arrive every 20 ms, with
+ * the timestamp field incrementing by 160 units each time.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <pcap/pcap.h>
+
+static pcap_t *pcap;
+static in_addr_t match_ip_addr;
+static u_short match_udp_port;
+static unsigned iphdr_addr_offset, udphdr_port_offset;
+static unsigned link_hdr_len, ethertype_offset;
+static int stream_init_flag, deltat_init_flag;
+static unsigned last_seq, last_tstamp, stream_ssrc;
+static struct timeval last_pkt_time;
+static unsigned deltat_min, deltat_max;
+
+static void
+check_dl_type()
+{
+	int dltype;
+
+	dltype = pcap_datalink(pcap);
+	switch (dltype) {
+	case DLT_EN10MB:
+		link_hdr_len = 14;
+		ethertype_offset = 12;
+		break;
+	case DLT_RAW:
+		link_hdr_len = 0;
+		break;
+	case DLT_LINUX_SLL:
+		link_hdr_len = 16;
+		ethertype_offset = 14;
+		break;
+	default:
+		fprintf(stderr, "error: unsupported data link type %d\n",
+			dltype);
+		exit(1);
+	}
+}
+
+static void
+rtp_stream_logic(rtp_hdr, pkt_idx, pkt_time)
+	u_char *rtp_hdr;
+	unsigned pkt_idx;
+	struct timeval *pkt_time;
+{
+	unsigned cur_seq, cur_tstamp, cur_ssrc;
+	struct timeval deltat;
+
+	cur_seq = (rtp_hdr[2] << 8) | rtp_hdr[3];
+	cur_tstamp = (rtp_hdr[4] << 24) | (rtp_hdr[5] << 16) |
+			(rtp_hdr[6] << 8) | rtp_hdr[7];
+	cur_ssrc = (rtp_hdr[8] << 24) | (rtp_hdr[9] << 16) |
+			(rtp_hdr[10] << 8) | rtp_hdr[11];
+	if (stream_init_flag) {
+		if (cur_ssrc != stream_ssrc) {
+			fprintf(stderr,
+		"error in packet #%u: SSRC change from 0x%08X to 0x%08X\n",
+				pkt_idx, stream_ssrc, cur_ssrc);
+			exit(1);
+		}
+		if (cur_seq != last_seq + 1 &&
+		    (cur_seq != 0 || last_seq != 0xFFFF)) {
+			fprintf(stderr,
+		"error in packet #%u: seq break from 0x%04X to 0x%04X\n",
+				pkt_idx, last_seq, cur_seq);
+			exit(1);
+		}
+		if (cur_tstamp != last_tstamp + 160) {
+			fprintf(stderr,
+		"error in packet #%u: timestamp break from 0x%08X to 0x%08X\n",
+				pkt_idx, last_tstamp, cur_tstamp);
+			exit(1);
+		}
+		if (timercmp(pkt_time, &last_pkt_time, <)) {
+			fprintf(stderr,
+			"packet #%u timing error: Rx time goes backward\n",
+				pkt_idx);
+			exit(1);
+		}
+		timersub(pkt_time, &last_pkt_time, &deltat);
+		if (deltat.tv_sec) {
+			fprintf(stderr,
+				"packet #%u timing error: Rx time gap >= 1 s\n",
+				pkt_idx);
+			exit(1);
+		}
+		if (deltat_init_flag) {
+			if (deltat.tv_usec < deltat_min)
+				deltat_min = deltat.tv_usec;
+			if (deltat.tv_usec > deltat_max)
+				deltat_max = deltat.tv_usec;
+		} else {
+			deltat_min = deltat.tv_usec;
+			deltat_max = deltat.tv_usec;
+			deltat_init_flag = 1;
+		}
+	} else {
+		stream_init_flag = 1;
+		stream_ssrc = cur_ssrc;
+	}
+	last_seq = cur_seq;
+	last_tstamp = cur_tstamp;
+	bcopy(pkt_time, &last_pkt_time, sizeof(struct timeval));
+}
+
+static void
+process_packet(pkt, caplen, pkt_idx, pkt_time)
+	u_char *pkt;
+	unsigned caplen, pkt_idx;
+	struct timeval *pkt_time;
+{
+	unsigned udplen;
+
+	if (caplen < link_hdr_len + 28)
+		return;
+	if (link_hdr_len) {
+		if (pkt[ethertype_offset] != 0x08)
+			return;
+		if (pkt[ethertype_offset+1] != 0x00)
+			return;
+		pkt += link_hdr_len;
+		caplen -= link_hdr_len;
+	}
+	/* check IP header */
+	if (pkt[0] != 0x45)
+		return;
+	if (pkt[9] != 17)	/* UDP */
+		return;
+	if (bcmp(pkt + iphdr_addr_offset, &match_ip_addr, 4))
+		return;
+	/* check UDP header */
+	if (bcmp(pkt + 20 + udphdr_port_offset, &match_udp_port, 2))
+		return;
+	/* it is our target - now scrutinize it */
+	udplen = (pkt[24] << 8) | pkt[25];
+	if (caplen < udplen + 20) {
+		fprintf(stderr,
+			"error: packet #%u is truncated in the capture\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (udplen < 20) {
+		fprintf(stderr,
+		"error in packet #%u: UDP length is too short for RTP header\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (pkt[28] != 0x80) {
+		fprintf(stderr,
+		"error in packet #%u: unsupported RTP header structure\n",
+			pkt_idx);
+		exit(1);
+	}
+	rtp_stream_logic(pkt + 28, pkt_idx, pkt_time);
+}
+
+main(argc, argv)
+	char **argv;
+{
+	char errbuf[PCAP_ERRBUF_SIZE];
+	u_char *pkt;
+	struct pcap_pkthdr pkthdr;
+	unsigned pkt_idx;
+
+	if (argc != 5) {
+		fprintf(stderr,
+			"usage: %s pcap-file src|dest ip-addr udp-port\n",
+			argv[0]);
+		exit(1);
+	}
+	pcap = pcap_open_offline(argv[1], errbuf);
+	if (!pcap) {
+		fprintf(stderr, "%s: %s\n", argv[1], errbuf);
+		exit(1);
+	}
+	check_dl_type();
+	if (!strcmp(argv[2], "src")) {
+		iphdr_addr_offset = 12;
+		udphdr_port_offset = 0;
+	} else if (!strcmp(argv[2], "dest")) {
+		iphdr_addr_offset = 16;
+		udphdr_port_offset = 2;
+	} else {
+		fprintf(stderr,
+		"error: direction argument must be \"src\" or \"dest\"\n");
+		exit(1);
+	}
+	match_ip_addr = inet_addr(argv[3]);
+	if (match_ip_addr == INADDR_NONE) {
+		fprintf(stderr, "error: IP address argument is invalid\n");
+		exit(1);
+	}
+	match_udp_port = htons(strtoul(argv[4], 0, 0));
+	for (pkt_idx = 0; ; pkt_idx++) {
+		pkt = pcap_next(pcap, &pkthdr);
+		if (!pkt)
+			break;
+		process_packet(pkt, (unsigned) pkthdr.caplen, pkt_idx,
+				&pkthdr.ts);
+	}
+	if (!stream_init_flag) {
+		fprintf(stderr, "error: specified RTP stream not found\n");
+		exit(1);
+	}
+	if (!deltat_init_flag) {
+		fprintf(stderr, "error: found only one matching packet\n");
+		exit(1);
+	}
+	printf("Minimum time delta: %u us\n", deltat_min);
+	printf("Maximum time delta: %u us\n", deltat_max);
+	exit(0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rtp-g711-extr.c	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,206 @@
+/*
+ * This program reads a pcap file, extracts packets belonging to a
+ * particular RTP stream as identified by a source or destination
+ * IP:port, and verifies that an unbroken RTP stream is present,
+ * with 160-byte payloads corresponding to timestamp increments
+ * of 160 units per packet, as expected for a G.711 (PCMU or PCMA)
+ * RTP stream with 20 ms packetization.  The extracted G.711 stream
+ * is written to a raw binary file.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <pcap/pcap.h>
+
+static pcap_t *pcap;
+static in_addr_t match_ip_addr;
+static u_short match_udp_port;
+static unsigned iphdr_addr_offset, udphdr_port_offset;
+static unsigned link_hdr_len, ethertype_offset;
+static FILE *outfile;
+static int stream_init_flag;
+static unsigned last_seq, last_tstamp, stream_ssrc;
+
+static void
+check_dl_type()
+{
+	int dltype;
+
+	dltype = pcap_datalink(pcap);
+	switch (dltype) {
+	case DLT_EN10MB:
+		link_hdr_len = 14;
+		ethertype_offset = 12;
+		break;
+	case DLT_RAW:
+		link_hdr_len = 0;
+		break;
+	case DLT_LINUX_SLL:
+		link_hdr_len = 16;
+		ethertype_offset = 14;
+		break;
+	default:
+		fprintf(stderr, "error: unsupported data link type %d\n",
+			dltype);
+		exit(1);
+	}
+}
+
+static void
+rtp_stream_logic(rtp_hdr, pkt_idx)
+	u_char *rtp_hdr;
+	unsigned pkt_idx;
+{
+	unsigned cur_seq, cur_tstamp, cur_ssrc;
+
+	cur_seq = (rtp_hdr[2] << 8) | rtp_hdr[3];
+	cur_tstamp = (rtp_hdr[4] << 24) | (rtp_hdr[5] << 16) |
+			(rtp_hdr[6] << 8) | rtp_hdr[7];
+	cur_ssrc = (rtp_hdr[8] << 24) | (rtp_hdr[9] << 16) |
+			(rtp_hdr[10] << 8) | rtp_hdr[11];
+	if (stream_init_flag) {
+		if (cur_ssrc != stream_ssrc) {
+			fprintf(stderr,
+		"error in packet #%u: SSRC change from 0x%08X to 0x%08X\n",
+				pkt_idx, stream_ssrc, cur_ssrc);
+			exit(1);
+		}
+		if (cur_seq != last_seq + 1 &&
+		    (cur_seq != 0 || last_seq != 0xFFFF)) {
+			fprintf(stderr,
+		"error in packet #%u: seq break from 0x%04X to 0x%04X\n",
+				pkt_idx, last_seq, cur_seq);
+			exit(1);
+		}
+		if (cur_tstamp != last_tstamp + 160) {
+			fprintf(stderr,
+		"error in packet #%u: timestamp break from 0x%08X to 0x%08X\n",
+				pkt_idx, last_tstamp, cur_tstamp);
+			exit(1);
+		}
+	} else {
+		stream_init_flag = 1;
+		stream_ssrc = cur_ssrc;
+	}
+	last_seq = cur_seq;
+	last_tstamp = cur_tstamp;
+}
+
+static void
+process_packet(pkt, caplen, pkt_idx)
+	u_char *pkt;
+	unsigned caplen, pkt_idx;
+{
+	unsigned udplen, payload_len;
+
+	if (caplen < link_hdr_len + 28)
+		return;
+	if (link_hdr_len) {
+		if (pkt[ethertype_offset] != 0x08)
+			return;
+		if (pkt[ethertype_offset+1] != 0x00)
+			return;
+		pkt += link_hdr_len;
+		caplen -= link_hdr_len;
+	}
+	/* check IP header */
+	if (pkt[0] != 0x45)
+		return;
+	if (pkt[9] != 17)	/* UDP */
+		return;
+	if (bcmp(pkt + iphdr_addr_offset, &match_ip_addr, 4))
+		return;
+	/* check UDP header */
+	if (bcmp(pkt + 20 + udphdr_port_offset, &match_udp_port, 2))
+		return;
+	/* it is our target - now scrutinize it */
+	udplen = (pkt[24] << 8) | pkt[25];
+	if (caplen < udplen + 20) {
+		fprintf(stderr,
+			"error: packet #%u is truncated in the capture\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (udplen < 20) {
+		fprintf(stderr,
+		"error in packet #%u: UDP length is too short for RTP header\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (pkt[28] != 0x80) {
+		fprintf(stderr,
+		"error in packet #%u: unsupported RTP header structure\n",
+			pkt_idx);
+		exit(1);
+	}
+	rtp_stream_logic(pkt + 28, pkt_idx);
+	payload_len = udplen - 20;
+	if (payload_len != 160) {
+		fprintf(stderr, "error in packet #%u: unsupported payload\n",
+			pkt_idx);
+		exit(1);
+	}
+	fwrite(pkt + 40, 1, payload_len, outfile);
+}
+
+main(argc, argv)
+	char **argv;
+{
+	char errbuf[PCAP_ERRBUF_SIZE];
+	u_char *pkt;
+	struct pcap_pkthdr pkthdr;
+	unsigned pkt_idx;
+
+	if (argc != 6) {
+		fprintf(stderr,
+		"usage: %s pcap-file src|dest ip-addr udp-port outfile\n",
+			argv[0]);
+		exit(1);
+	}
+	pcap = pcap_open_offline(argv[1], errbuf);
+	if (!pcap) {
+		fprintf(stderr, "%s: %s\n", argv[1], errbuf);
+		exit(1);
+	}
+	check_dl_type();
+	if (!strcmp(argv[2], "src")) {
+		iphdr_addr_offset = 12;
+		udphdr_port_offset = 0;
+	} else if (!strcmp(argv[2], "dest")) {
+		iphdr_addr_offset = 16;
+		udphdr_port_offset = 2;
+	} else {
+		fprintf(stderr,
+		"error: direction argument must be \"src\" or \"dest\"\n");
+		exit(1);
+	}
+	match_ip_addr = inet_addr(argv[3]);
+	if (match_ip_addr == INADDR_NONE) {
+		fprintf(stderr, "error: IP address argument is invalid\n");
+		exit(1);
+	}
+	match_udp_port = htons(strtoul(argv[4], 0, 0));
+	outfile = fopen(argv[5], "w");
+	if (!outfile) {
+		perror(argv[5]);
+		exit(1);
+	}
+	for (pkt_idx = 0; ; pkt_idx++) {
+		pkt = pcap_next(pcap, &pkthdr);
+		if (!pkt)
+			break;
+		process_packet(pkt, (unsigned) pkthdr.caplen, pkt_idx);
+	}
+	if (!stream_init_flag) {
+		fprintf(stderr, "error: specified RTP stream not found\n");
+		exit(1);
+	}
+	fclose(outfile);
+	exit(0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rtp-gsmfr-extr.c	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,219 @@
+/*
+ * This program reads a pcap file, extracts packets belonging to a
+ * particular RTP stream as identified by a source or destination
+ * IP:port, verifies that an unbroken RTP stream is present, in
+ * GSM FR or EFR codec, and writes the FR/EFR frame stream to our
+ * extended-libgsm format binary file, to be fed to decoding test
+ * programs.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <pcap/pcap.h>
+
+static pcap_t *pcap;
+static in_addr_t match_ip_addr;
+static u_short match_udp_port;
+static unsigned iphdr_addr_offset, udphdr_port_offset;
+static unsigned link_hdr_len, ethertype_offset;
+static FILE *outfile;
+static int stream_init_flag;
+static unsigned last_seq, last_tstamp, stream_ssrc;
+
+static void
+check_dl_type()
+{
+	int dltype;
+
+	dltype = pcap_datalink(pcap);
+	switch (dltype) {
+	case DLT_EN10MB:
+		link_hdr_len = 14;
+		ethertype_offset = 12;
+		break;
+	case DLT_RAW:
+		link_hdr_len = 0;
+		break;
+	case DLT_LINUX_SLL:
+		link_hdr_len = 16;
+		ethertype_offset = 14;
+		break;
+	default:
+		fprintf(stderr, "error: unsupported data link type %d\n",
+			dltype);
+		exit(1);
+	}
+}
+
+static void
+rtp_stream_logic(rtp_hdr, pkt_idx)
+	u_char *rtp_hdr;
+	unsigned pkt_idx;
+{
+	unsigned cur_seq, cur_tstamp, cur_ssrc;
+
+	cur_seq = (rtp_hdr[2] << 8) | rtp_hdr[3];
+	cur_tstamp = (rtp_hdr[4] << 24) | (rtp_hdr[5] << 16) |
+			(rtp_hdr[6] << 8) | rtp_hdr[7];
+	cur_ssrc = (rtp_hdr[8] << 24) | (rtp_hdr[9] << 16) |
+			(rtp_hdr[10] << 8) | rtp_hdr[11];
+	if (stream_init_flag) {
+		if (cur_ssrc != stream_ssrc) {
+			fprintf(stderr,
+		"error in packet #%u: SSRC change from 0x%08X to 0x%08X\n",
+				pkt_idx, stream_ssrc, cur_ssrc);
+			exit(1);
+		}
+		if (cur_seq != last_seq + 1 &&
+		    (cur_seq != 0 || last_seq != 0xFFFF)) {
+			fprintf(stderr,
+		"error in packet #%u: seq break from 0x%04X to 0x%04X\n",
+				pkt_idx, last_seq, cur_seq);
+			exit(1);
+		}
+		if (cur_tstamp != last_tstamp + 160) {
+			fprintf(stderr,
+		"error in packet #%u: timestamp break from 0x%08X to 0x%08X\n",
+				pkt_idx, last_tstamp, cur_tstamp);
+			exit(1);
+		}
+	} else {
+		stream_init_flag = 1;
+		stream_ssrc = cur_ssrc;
+	}
+	last_seq = cur_seq;
+	last_tstamp = cur_tstamp;
+}
+
+static void
+process_packet(pkt, caplen, pkt_idx)
+	u_char *pkt;
+	unsigned caplen, pkt_idx;
+{
+	unsigned udplen, payload_len;
+
+	if (caplen < link_hdr_len + 28)
+		return;
+	if (link_hdr_len) {
+		if (pkt[ethertype_offset] != 0x08)
+			return;
+		if (pkt[ethertype_offset+1] != 0x00)
+			return;
+		pkt += link_hdr_len;
+		caplen -= link_hdr_len;
+	}
+	/* check IP header */
+	if (pkt[0] != 0x45)
+		return;
+	if (pkt[9] != 17)	/* UDP */
+		return;
+	if (bcmp(pkt + iphdr_addr_offset, &match_ip_addr, 4))
+		return;
+	/* check UDP header */
+	if (bcmp(pkt + 20 + udphdr_port_offset, &match_udp_port, 2))
+		return;
+	/* it is our target - now scrutinize it */
+	udplen = (pkt[24] << 8) | pkt[25];
+	if (caplen < udplen + 20) {
+		fprintf(stderr,
+			"error: packet #%u is truncated in the capture\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (udplen < 20) {
+		fprintf(stderr,
+		"error in packet #%u: UDP length is too short for RTP header\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (pkt[28] != 0x80) {
+		fprintf(stderr,
+		"error in packet #%u: unsupported RTP header structure\n",
+			pkt_idx);
+		exit(1);
+	}
+	rtp_stream_logic(pkt + 28, pkt_idx);
+	payload_len = udplen - 20;
+	switch (payload_len) {
+	case 2:
+		if (pkt[40] == 0xBF)
+			break;
+		goto bad_payload;
+	case 31:
+		if ((pkt[40] & 0xF0) == 0xC0)
+			break;
+		goto bad_payload;
+	case 33:
+		if ((pkt[40] & 0xF0) == 0xD0)
+			break;
+		/* FALL THRU */
+	default:
+	bad_payload:
+		fprintf(stderr, "error in packet #%u: unsupported payload\n",
+			pkt_idx);
+		exit(1);
+	}
+	fwrite(pkt + 40, 1, payload_len, outfile);
+}
+
+main(argc, argv)
+	char **argv;
+{
+	char errbuf[PCAP_ERRBUF_SIZE];
+	u_char *pkt;
+	struct pcap_pkthdr pkthdr;
+	unsigned pkt_idx;
+
+	if (argc != 6) {
+		fprintf(stderr,
+		"usage: %s pcap-file src|dest ip-addr udp-port outfile\n",
+			argv[0]);
+		exit(1);
+	}
+	pcap = pcap_open_offline(argv[1], errbuf);
+	if (!pcap) {
+		fprintf(stderr, "%s: %s\n", argv[1], errbuf);
+		exit(1);
+	}
+	check_dl_type();
+	if (!strcmp(argv[2], "src")) {
+		iphdr_addr_offset = 12;
+		udphdr_port_offset = 0;
+	} else if (!strcmp(argv[2], "dest")) {
+		iphdr_addr_offset = 16;
+		udphdr_port_offset = 2;
+	} else {
+		fprintf(stderr,
+		"error: direction argument must be \"src\" or \"dest\"\n");
+		exit(1);
+	}
+	match_ip_addr = inet_addr(argv[3]);
+	if (match_ip_addr == INADDR_NONE) {
+		fprintf(stderr, "error: IP address argument is invalid\n");
+		exit(1);
+	}
+	match_udp_port = htons(strtoul(argv[4], 0, 0));
+	outfile = fopen(argv[5], "w");
+	if (!outfile) {
+		perror(argv[5]);
+		exit(1);
+	}
+	for (pkt_idx = 0; ; pkt_idx++) {
+		pkt = pcap_next(pcap, &pkthdr);
+		if (!pkt)
+			break;
+		process_packet(pkt, (unsigned) pkthdr.caplen, pkt_idx);
+	}
+	if (!stream_init_flag) {
+		fprintf(stderr, "error: specified RTP stream not found\n");
+		exit(1);
+	}
+	fclose(outfile);
+	exit(0);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rtp-jitter-view.c	Sun Apr 02 00:28:28 2023 +0000
@@ -0,0 +1,220 @@
+/*
+ * This program reads a pcap file, extracts packets belonging to a
+ * particular RTP stream as identified by a source or destination
+ * IP:port, checks its continuity at the packet header level
+ * (verifies that the sequence number always increments by 1 and
+ * that the timestamp always increments by 160 units) and prints out
+ * the Rx time delta of each stream packet (the capture time of
+ * the current packet minus the capture time of the previous packet)
+ * on stdout, allowing visual analysis of timing jitter.
+ *
+ * The codec can be anything, and this program can also be used to
+ * examine the jitter of RTP streams coming from PSTN/SIP, but the
+ * core assumption is that packets must arrive every 20 ms, with
+ * the timestamp field incrementing by 160 units each time.
+ */
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <pcap/pcap.h>
+
+static pcap_t *pcap;
+static in_addr_t match_ip_addr;
+static u_short match_udp_port;
+static unsigned iphdr_addr_offset, udphdr_port_offset;
+static unsigned link_hdr_len, ethertype_offset;
+static int stream_init_flag;
+static unsigned last_seq, last_tstamp, stream_ssrc;
+static struct timeval last_pkt_time;
+
+static void
+check_dl_type()
+{
+	int dltype;
+
+	dltype = pcap_datalink(pcap);
+	switch (dltype) {
+	case DLT_EN10MB:
+		link_hdr_len = 14;
+		ethertype_offset = 12;
+		break;
+	case DLT_RAW:
+		link_hdr_len = 0;
+		break;
+	case DLT_LINUX_SLL:
+		link_hdr_len = 16;
+		ethertype_offset = 14;
+		break;
+	default:
+		fprintf(stderr, "error: unsupported data link type %d\n",
+			dltype);
+		exit(1);
+	}
+}
+
+static void
+rtp_stream_logic(rtp_hdr, pkt_idx, pkt_time)
+	u_char *rtp_hdr;
+	unsigned pkt_idx;
+	struct timeval *pkt_time;
+{
+	unsigned cur_seq, cur_tstamp, cur_ssrc;
+	struct timeval deltat;
+
+	cur_seq = (rtp_hdr[2] << 8) | rtp_hdr[3];
+	cur_tstamp = (rtp_hdr[4] << 24) | (rtp_hdr[5] << 16) |
+			(rtp_hdr[6] << 8) | rtp_hdr[7];
+	cur_ssrc = (rtp_hdr[8] << 24) | (rtp_hdr[9] << 16) |
+			(rtp_hdr[10] << 8) | rtp_hdr[11];
+	if (stream_init_flag) {
+		if (cur_ssrc != stream_ssrc) {
+			fprintf(stderr,
+		"error in packet #%u: SSRC change from 0x%08X to 0x%08X\n",
+				pkt_idx, stream_ssrc, cur_ssrc);
+			exit(1);
+		}
+		if (cur_seq != last_seq + 1 &&
+		    (cur_seq != 0 || last_seq != 0xFFFF)) {
+			fprintf(stderr,
+		"error in packet #%u: seq break from 0x%04X to 0x%04X\n",
+				pkt_idx, last_seq, cur_seq);
+			exit(1);
+		}
+		if (cur_tstamp != last_tstamp + 160) {
+			fprintf(stderr,
+		"error in packet #%u: timestamp break from 0x%08X to 0x%08X\n",
+				pkt_idx, last_tstamp, cur_tstamp);
+			exit(1);
+		}
+		if (timercmp(pkt_time, &last_pkt_time, <)) {
+			fprintf(stderr,
+			"packet #%u timing error: Rx time goes backward\n",
+				pkt_idx);
+			exit(1);
+		}
+		timersub(pkt_time, &last_pkt_time, &deltat);
+		if (deltat.tv_sec) {
+			fprintf(stderr,
+				"packet #%u timing error: Rx time gap >= 1 s\n",
+				pkt_idx);
+			exit(1);
+		}
+		printf("Packet #%u: time delta %u us\n", pkt_idx,
+			(unsigned) deltat.tv_usec);
+	} else {
+		stream_init_flag = 1;
+		stream_ssrc = cur_ssrc;
+	}
+	last_seq = cur_seq;
+	last_tstamp = cur_tstamp;
+	bcopy(pkt_time, &last_pkt_time, sizeof(struct timeval));
+}
+
+static void
+process_packet(pkt, caplen, pkt_idx, pkt_time)
+	u_char *pkt;
+	unsigned caplen, pkt_idx;
+	struct timeval *pkt_time;
+{
+	unsigned udplen;
+
+	if (caplen < link_hdr_len + 28)
+		return;
+	if (link_hdr_len) {
+		if (pkt[ethertype_offset] != 0x08)
+			return;
+		if (pkt[ethertype_offset+1] != 0x00)
+			return;
+		pkt += link_hdr_len;
+		caplen -= link_hdr_len;
+	}
+	/* check IP header */
+	if (pkt[0] != 0x45)
+		return;
+	if (pkt[9] != 17)	/* UDP */
+		return;
+	if (bcmp(pkt + iphdr_addr_offset, &match_ip_addr, 4))
+		return;
+	/* check UDP header */
+	if (bcmp(pkt + 20 + udphdr_port_offset, &match_udp_port, 2))
+		return;
+	/* it is our target - now scrutinize it */
+	udplen = (pkt[24] << 8) | pkt[25];
+	if (caplen < udplen + 20) {
+		fprintf(stderr,
+			"error: packet #%u is truncated in the capture\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (udplen < 20) {
+		fprintf(stderr,
+		"error in packet #%u: UDP length is too short for RTP header\n",
+			pkt_idx);
+		exit(1);
+	}
+	if (pkt[28] != 0x80) {
+		fprintf(stderr,
+		"error in packet #%u: unsupported RTP header structure\n",
+			pkt_idx);
+		exit(1);
+	}
+	rtp_stream_logic(pkt + 28, pkt_idx, pkt_time);
+}
+
+main(argc, argv)
+	char **argv;
+{
+	char errbuf[PCAP_ERRBUF_SIZE];
+	u_char *pkt;
+	struct pcap_pkthdr pkthdr;
+	unsigned pkt_idx;
+
+	if (argc != 5) {
+		fprintf(stderr,
+			"usage: %s pcap-file src|dest ip-addr udp-port\n",
+			argv[0]);
+		exit(1);
+	}
+	pcap = pcap_open_offline(argv[1], errbuf);
+	if (!pcap) {
+		fprintf(stderr, "%s: %s\n", argv[1], errbuf);
+		exit(1);
+	}
+	check_dl_type();
+	if (!strcmp(argv[2], "src")) {
+		iphdr_addr_offset = 12;
+		udphdr_port_offset = 0;
+	} else if (!strcmp(argv[2], "dest")) {
+		iphdr_addr_offset = 16;
+		udphdr_port_offset = 2;
+	} else {
+		fprintf(stderr,
+		"error: direction argument must be \"src\" or \"dest\"\n");
+		exit(1);
+	}
+	match_ip_addr = inet_addr(argv[3]);
+	if (match_ip_addr == INADDR_NONE) {
+		fprintf(stderr, "error: IP address argument is invalid\n");
+		exit(1);
+	}
+	match_udp_port = htons(strtoul(argv[4], 0, 0));
+	for (pkt_idx = 0; ; pkt_idx++) {
+		pkt = pcap_next(pcap, &pkthdr);
+		if (!pkt)
+			break;
+		process_packet(pkt, (unsigned) pkthdr.caplen, pkt_idx,
+				&pkthdr.ts);
+	}
+	if (!stream_init_flag) {
+		fprintf(stderr, "error: specified RTP stream not found\n");
+		exit(1);
+	}
+	exit(0);
+}