509 lines
15 KiB
Plaintext
509 lines
15 KiB
Plaintext
module RTP_Emulation {
|
|
|
|
/* Functionalities that we want this module to imeplement:
|
|
* * act as a RTP source that generates a RTP Stream
|
|
* * act asaa RTP sink that consumes a RTP Stream
|
|
*
|
|
* for all of the above, we want to be able to
|
|
* * specify the payload type
|
|
* * specify the interval / sample rate
|
|
* * create drop-outs in the stream
|
|
* * detect reordered or lost frames
|
|
* * validate if the size of the frames matches epectations
|
|
* * play back real audio (at least some tones?)
|
|
* * enable/disable generation/verification of RTCP
|
|
*/
|
|
|
|
/* (C) 2017-2018 Harald Welte <laforge@gnumonks.org>
|
|
* (C) 2018-2019 sysmocom - s.f.m.c. GmbH
|
|
* All rights reserved.
|
|
*
|
|
* Released under the terms of GNU General Public License, Version 2 or
|
|
* (at your option) any later version.
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
|
|
/* Ideas:
|
|
|
|
* each component consists of transmitter and receiver
|
|
* transmitters and receivers can be operated as tuple?
|
|
* high-level operation
|
|
** set-up config at transmitter + receiver
|
|
** transmit sequence of payloads
|
|
** verify receiption of those payloads
|
|
* can operate full-duplex/bi-directional as needed
|
|
|
|
* transmitter
|
|
** trigger transmission of n number of packets
|
|
** transmit them at normal ptime interval
|
|
** payload size configurable
|
|
** payload contents PRBS or the like
|
|
|
|
* receiver
|
|
** count number of related packets at receiver
|
|
** check received payload type
|
|
** check received timestamp increments
|
|
** check received seq_nr increments
|
|
** (optionally) check for SSRC
|
|
** (optionally) check for payload size
|
|
** (optionally) check for payload contents
|
|
|
|
* later
|
|
** how to test transcoding?
|
|
** how to test pure play-out endpoints (rx only)?
|
|
** how to test "Rx from wrong IP/port" scenarios?
|
|
** how to test RTCP?
|
|
** maybe keep ports un-connected to show wrong src -lrt
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import from General_Types all;
|
|
import from Osmocom_Types all;
|
|
import from IPL4asp_Types all;
|
|
import from RTP_Types all;
|
|
import from RTP_CodecPort all;
|
|
import from RTP_CodecPort_CtrlFunct all;
|
|
|
|
import from IuUP_Types all;
|
|
import from IuUP_Emulation all;
|
|
|
|
type component RTP_Emulation_CT {
|
|
/* down-facing ports for RTP and RTCP codec ports on top of IPL4asp */
|
|
port RTP_CODEC_PT RTP;
|
|
var integer g_rtp_conn_id := -1;
|
|
port RTP_CODEC_PT RTCP;
|
|
var integer g_rtcp_conn_id := -1;
|
|
|
|
/* user-facing port for controlling the binding */
|
|
port RTPEM_CTRL_PT CTRL;
|
|
|
|
/* configurable by user, should be fixed */
|
|
var RtpemConfig g_cfg := c_RtpemDefaultCfg;
|
|
|
|
/* statistics */
|
|
var RtpemStats g_stats_rtp := c_RtpemStatsReset;
|
|
var RtpemStats g_stats_rtcp := c_RtpemStatsReset;
|
|
|
|
var HostName g_remote_host;
|
|
var PortNumber g_remote_port;
|
|
var HostName g_local_host;
|
|
var PortNumber g_local_port;
|
|
|
|
/* state variables, change over time */
|
|
var boolean g_rx_enabled := false;
|
|
var boolean g_tx_connected := false; /* Set to true after connect() */
|
|
var LIN2_BO_LAST g_tx_next_seq := 0;
|
|
var uint32_t g_tx_next_ts := 0;
|
|
|
|
var INT7b g_rx_payload_type := 0;
|
|
var LIN2_BO_LAST g_rx_last_seq;
|
|
var uint32_t g_rx_last_ts;
|
|
|
|
var IuUP_Entity g_iuup_ent; // := valueof(t_IuUP_Entity(1));
|
|
}
|
|
|
|
type enumerated RtpemMode {
|
|
RTPEM_MODE_NONE,
|
|
RTPEM_MODE_TXONLY,
|
|
RTPEM_MODE_RXONLY,
|
|
RTPEM_MODE_BIDIR
|
|
};
|
|
|
|
type record RtpemStats {
|
|
/* number of packets transmitted */
|
|
integer num_pkts_tx,
|
|
/* number of RTP payload bytes transmitted */
|
|
integer bytes_payload_tx,
|
|
|
|
/* number of packets received */
|
|
integer num_pkts_rx,
|
|
/* number of RTP payload bytes received */
|
|
integer bytes_payload_rx,
|
|
/* number of packets received out-of-sequence */
|
|
integer num_pkts_rx_err_seq,
|
|
/* number of packets received wrong timestamp */
|
|
integer num_pkts_rx_err_ts,
|
|
/* number of packets received wrong payload type */
|
|
integer num_pkts_rx_err_pt,
|
|
/* number of packets received during Rx disable */
|
|
integer num_pkts_rx_err_disabled,
|
|
/* number of packets received with mismatching payload */
|
|
integer num_pkts_rx_err_payload
|
|
}
|
|
|
|
const RtpemStats c_RtpemStatsReset := {
|
|
num_pkts_tx := 0,
|
|
bytes_payload_tx := 0,
|
|
num_pkts_rx := 0,
|
|
bytes_payload_rx := 0,
|
|
num_pkts_rx_err_seq := 0,
|
|
num_pkts_rx_err_ts := 0,
|
|
num_pkts_rx_err_pt := 0,
|
|
num_pkts_rx_err_disabled := 0,
|
|
num_pkts_rx_err_payload := 0
|
|
}
|
|
|
|
type record RtpemConfig {
|
|
INT7b tx_payload_type,
|
|
integer tx_samplerate_hz,
|
|
integer tx_duration_ms,
|
|
BIT32_BO_LAST tx_ssrc,
|
|
octetstring tx_fixed_payload optional,
|
|
octetstring rx_fixed_payload optional,
|
|
boolean iuup_mode,
|
|
boolean iuup_tx_init
|
|
};
|
|
|
|
const RtpemConfig c_RtpemDefaultCfg := {
|
|
tx_payload_type := 0,
|
|
tx_samplerate_hz := 8000,
|
|
tx_duration_ms := 20,
|
|
tx_ssrc := '11011110101011011011111011101111'B,
|
|
tx_fixed_payload := '01020304'O,
|
|
rx_fixed_payload := '01020304'O,
|
|
iuup_mode := false,
|
|
iuup_tx_init := true
|
|
}
|
|
|
|
signature RTPEM_bind(in HostName local_host, inout PortNumber local_port);
|
|
signature RTPEM_connect(in HostName remote_host, in PortNumber remote_port);
|
|
signature RTPEM_mode(in RtpemMode mode);
|
|
signature RTPEM_configure(in RtpemConfig cfg);
|
|
signature RTPEM_stats_get(out RtpemStats stats, in boolean rtcp);
|
|
|
|
type port RTPEM_CTRL_PT procedure {
|
|
inout RTPEM_bind, RTPEM_connect, RTPEM_mode, RTPEM_configure, RTPEM_stats_get;
|
|
} with { extension "internal" };
|
|
|
|
function f_rtpem_bind(RTPEM_CTRL_PT pt, in HostName local_host, inout PortNumber local_port) {
|
|
pt.call(RTPEM_bind:{local_host, local_port}) {
|
|
[] pt.getreply(RTPEM_bind:{local_host, ?}) -> param (local_port) {};
|
|
}
|
|
}
|
|
function f_rtpem_connect(RTPEM_CTRL_PT pt, in HostName remote_host, in PortNumber remote_port) {
|
|
pt.call(RTPEM_connect:{remote_host, remote_port}) {
|
|
[] pt.getreply(RTPEM_connect:{remote_host, remote_port}) {};
|
|
}
|
|
}
|
|
function f_rtpem_mode(RTPEM_CTRL_PT pt, in RtpemMode mode) {
|
|
pt.call(RTPEM_mode:{mode}) {
|
|
[] pt.getreply(RTPEM_mode:{mode}) {};
|
|
}
|
|
}
|
|
function f_rtpem_configure(RTPEM_CTRL_PT pt, in RtpemConfig cfg) {
|
|
pt.call(RTPEM_configure:{cfg}) {
|
|
[] pt.getreply(RTPEM_configure:{cfg}) {};
|
|
}
|
|
}
|
|
function f_rtpem_stats_get(RTPEM_CTRL_PT pt, boolean rtcp := false) return RtpemStats {
|
|
var RtpemStats stats;
|
|
pt.call(RTPEM_stats_get:{-, rtcp}) {
|
|
[] pt.getreply(RTPEM_stats_get:{?, rtcp}) -> param(stats) {};
|
|
}
|
|
return stats;
|
|
}
|
|
|
|
function f_rtpem_stats_compare_value(integer a, integer b, integer tolerance := 0) return boolean {
|
|
var integer temp;
|
|
|
|
temp := (a - b)
|
|
if (temp < 0) {
|
|
temp := -temp;
|
|
}
|
|
|
|
if (temp > tolerance) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Cross-compare two rtpem-statistics. The transmission statistics on the a side
|
|
* must match the reception statistics on the other side and vice versa. The
|
|
* user may also supply a tolerance value (number of packets) when deviations
|
|
* are acceptable */
|
|
function f_rtpem_stats_compare(RtpemStats a, RtpemStats b, integer tolerance := 0) return boolean {
|
|
var integer plen;
|
|
|
|
log("stats A: ", a);
|
|
log("stats B: ", b);
|
|
log("tolerance: ", tolerance, " packets");
|
|
|
|
if (f_rtpem_stats_compare_value(a.num_pkts_tx, b.num_pkts_rx, tolerance) == false) {
|
|
return false;
|
|
}
|
|
|
|
if (f_rtpem_stats_compare_value(a.num_pkts_rx, b.num_pkts_tx, tolerance) == false) {
|
|
return false;
|
|
}
|
|
|
|
if(a.num_pkts_tx > 0) {
|
|
plen := a.bytes_payload_tx / a.num_pkts_tx;
|
|
} else {
|
|
plen := 0;
|
|
}
|
|
|
|
if (f_rtpem_stats_compare_value(a.bytes_payload_tx, b.bytes_payload_rx, tolerance * plen) == false) {
|
|
return false;
|
|
}
|
|
|
|
if (f_rtpem_stats_compare_value(a.bytes_payload_rx, b.bytes_payload_tx, tolerance * plen) == false) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Check the statistics for general signs of errors. This is a basic general
|
|
* check that will fit most situations and is intended to be executed by
|
|
* the testcases as as needed. */
|
|
function f_rtpem_stats_err_check(RtpemStats s) {
|
|
log("stats: ", s);
|
|
|
|
/* Check if there was some activity at either on the RX or on the
|
|
* TX side, but complete silence would indicate some problem */
|
|
if (s.num_pkts_tx < 1 and s.num_pkts_rx < 1) {
|
|
setverdict(fail, "no RTP packet activity detected (packets)");
|
|
mtc.stop;
|
|
}
|
|
if (s.bytes_payload_tx < 1 and s.bytes_payload_rx < 1) {
|
|
setverdict(fail, "no RTP packet activity detected (bytes)");
|
|
mtc.stop;
|
|
}
|
|
|
|
/* Check error counters */
|
|
if (s.num_pkts_rx_err_seq != 0) {
|
|
setverdict(fail, "RTP packet sequence number errors occurred");
|
|
mtc.stop;
|
|
}
|
|
if (s.num_pkts_rx_err_ts != 0) {
|
|
setverdict(fail, "RTP packet timestamp errors occurred");
|
|
mtc.stop;
|
|
}
|
|
if (s.num_pkts_rx_err_pt != 0) {
|
|
setverdict(fail, "RTP packet payload type errors occurred");
|
|
mtc.stop;
|
|
}
|
|
if (s.num_pkts_rx_err_disabled != 0) {
|
|
setverdict(fail, "RTP packets received while RX was disabled");
|
|
mtc.stop;
|
|
}
|
|
if (s.num_pkts_rx_err_payload != 0) {
|
|
setverdict(fail, "RTP packets with mismatching payload received");
|
|
mtc.stop;
|
|
}
|
|
}
|
|
|
|
template PDU_RTP ts_RTP(BIT32_BO_LAST ssrc, INT7b pt, LIN2_BO_LAST seq, uint32_t ts,
|
|
octetstring payload, BIT1 marker := '0'B) := {
|
|
version := 2,
|
|
padding_ind := '0'B,
|
|
extension_ind := '0'B,
|
|
CSRC_count := 0,
|
|
marker_bit := marker,
|
|
payload_type := pt,
|
|
sequence_number := seq,
|
|
time_stamp := int2bit(ts, 32),
|
|
SSRC_id := ssrc,
|
|
CSRCs := omit,
|
|
ext_header := omit,
|
|
data := payload
|
|
}
|
|
|
|
private function f_tx_rtp(octetstring payload, BIT1 marker := '0'B) runs on RTP_Emulation_CT {
|
|
if (g_cfg.iuup_mode) {
|
|
payload := f_IuUP_Em_tx_encap(g_iuup_ent, payload);
|
|
}
|
|
var PDU_RTP rtp := valueof(ts_RTP(g_cfg.tx_ssrc, g_cfg.tx_payload_type, g_tx_next_seq,
|
|
g_tx_next_ts, payload, marker));
|
|
RTP.send(t_RTP_Send(g_rtp_conn_id, RTP_messages_union:{rtp:=rtp}));
|
|
/* increment sequence + timestamp for next transmit */
|
|
g_tx_next_seq := g_tx_next_seq + 1;
|
|
g_tx_next_ts := g_tx_next_ts + (g_cfg.tx_samplerate_hz / (1000 / g_cfg.tx_duration_ms));
|
|
}
|
|
|
|
function f_main() runs on RTP_Emulation_CT
|
|
{
|
|
var Result res;
|
|
var boolean is_rtcp
|
|
|
|
timer T_transmit := int2float(g_cfg.tx_duration_ms)/1000.0;
|
|
var RTP_RecvFrom rx_rtp;
|
|
var RtpemConfig cfg;
|
|
var template RTP_RecvFrom tr := {
|
|
connId := ?,
|
|
remName := ?,
|
|
remPort := ?,
|
|
locName := ?,
|
|
locPort := ?,
|
|
msg := ?
|
|
};
|
|
var template RTP_RecvFrom tr_rtp := tr;
|
|
var template RTP_RecvFrom tr_rtcp := tr;
|
|
tr_rtp.msg := { rtp := ? };
|
|
tr_rtcp.msg := { rtcp := ? };
|
|
|
|
g_iuup_ent := valueof(t_IuUP_Entity(g_cfg.iuup_tx_init));
|
|
|
|
while (true) {
|
|
alt {
|
|
/* control procedures (calls) from the user */
|
|
[] CTRL.getcall(RTPEM_bind:{?,?}) -> param(g_local_host, g_local_port) {
|
|
if (g_local_port rem 2 == 1) {
|
|
//CTRL.raise(RTPEM_bind, "Local Port is not an even port number!");
|
|
log("Local Port is not an even port number!");
|
|
continue;
|
|
}
|
|
|
|
g_tx_connected := false; /* will set it back to true upon next connect() call */
|
|
res := RTP_CodecPort_CtrlFunct.f_IPL4_listen(RTP, g_local_host,
|
|
g_local_port, {udp:={}});
|
|
if (not ispresent(res.connId)) {
|
|
setverdict(fail, "Could not listen on RTP socket, check your configuration");
|
|
mtc.stop;
|
|
}
|
|
g_rtp_conn_id := res.connId;
|
|
tr_rtp.connId := g_rtp_conn_id;
|
|
res := RTP_CodecPort_CtrlFunct.f_IPL4_listen(RTCP, g_local_host,
|
|
g_local_port+1, {udp:={}});
|
|
if (not ispresent(res.connId)) {
|
|
setverdict(fail, "Could not listen on RTCP socket, check your configuration");
|
|
mtc.stop;
|
|
}
|
|
g_rtcp_conn_id := res.connId;
|
|
tr_rtcp.connId := g_rtcp_conn_id;
|
|
CTRL.reply(RTPEM_bind:{g_local_host, g_local_port});
|
|
}
|
|
[] CTRL.getcall(RTPEM_connect:{?,?}) -> param (g_remote_host, g_remote_port) {
|
|
if (g_remote_port rem 2 == 1) {
|
|
//CTRL.raise(RTPEM_connect, "Remote Port is not an even number!");
|
|
log("Remote Port is not an even number!");
|
|
continue;
|
|
}
|
|
res := RTP_CodecPort_CtrlFunct.f_IPL4_connect(RTP, g_remote_host,
|
|
g_remote_port,
|
|
g_local_host, g_local_port,
|
|
g_rtp_conn_id, {udp:={}});
|
|
if (not ispresent(res.connId)) {
|
|
setverdict(fail, "Could not connect to RTP socket, check your configuration");
|
|
mtc.stop;
|
|
}
|
|
res := RTP_CodecPort_CtrlFunct.f_IPL4_connect(RTCP, g_remote_host,
|
|
g_remote_port+1,
|
|
g_local_host, g_local_port+1,
|
|
g_rtcp_conn_id, {udp:={}});
|
|
if (not ispresent(res.connId)) {
|
|
setverdict(fail, "Could not connect to RTCP socket, check your configuration");
|
|
mtc.stop;
|
|
}
|
|
g_tx_connected := true;
|
|
CTRL.reply(RTPEM_connect:{g_remote_host, g_remote_port});
|
|
}
|
|
[] CTRL.getcall(RTPEM_mode:{RTPEM_MODE_NONE}) {
|
|
T_transmit.stop;
|
|
g_rx_enabled := false;
|
|
CTRL.reply(RTPEM_mode:{RTPEM_MODE_NONE});
|
|
}
|
|
[] CTRL.getcall(RTPEM_mode:{RTPEM_MODE_TXONLY}) {
|
|
/* start transmit timer */
|
|
T_transmit.start;
|
|
g_rx_enabled := false;
|
|
CTRL.reply(RTPEM_mode:{RTPEM_MODE_TXONLY});
|
|
}
|
|
[] CTRL.getcall(RTPEM_mode:{RTPEM_MODE_RXONLY}) {
|
|
|
|
T_transmit.stop;
|
|
if (g_rx_enabled == false) {
|
|
/* flush queues */
|
|
RTP.clear;
|
|
RTCP.clear;
|
|
g_rx_enabled := true;
|
|
}
|
|
CTRL.reply(RTPEM_mode:{RTPEM_MODE_RXONLY});
|
|
}
|
|
[] CTRL.getcall(RTPEM_mode:{RTPEM_MODE_BIDIR}) {
|
|
T_transmit.start;
|
|
if (g_rx_enabled == false) {
|
|
/* flush queues */
|
|
RTP.clear;
|
|
RTCP.clear;
|
|
g_rx_enabled := true;
|
|
}
|
|
CTRL.reply(RTPEM_mode:{RTPEM_MODE_BIDIR});
|
|
}
|
|
[] CTRL.getcall(RTPEM_configure:{?}) -> param (cfg) {
|
|
g_cfg := cfg;
|
|
g_iuup_ent.cfg.active_init := g_cfg.iuup_tx_init;
|
|
CTRL.reply(RTPEM_configure:{cfg});
|
|
}
|
|
[] CTRL.getcall(RTPEM_stats_get:{?, ?}) -> param (is_rtcp) {
|
|
if (is_rtcp) {
|
|
CTRL.reply(RTPEM_stats_get:{g_stats_rtcp, is_rtcp});
|
|
} else {
|
|
CTRL.reply(RTPEM_stats_get:{g_stats_rtp, is_rtcp});
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* simply ignore any RTTP/RTP if receiver not enabled */
|
|
[g_rx_enabled==false] RTP.receive(tr_rtp) {
|
|
g_stats_rtp.num_pkts_rx_err_disabled := g_stats_rtp.num_pkts_rx_err_disabled+1;
|
|
}
|
|
[g_rx_enabled==false] RTCP.receive(tr_rtcp) {
|
|
g_stats_rtcp.num_pkts_rx_err_disabled := g_stats_rtcp.num_pkts_rx_err_disabled+1;
|
|
}
|
|
|
|
/* process received RTCP/RTP if receiver enabled */
|
|
[g_rx_enabled] RTP.receive(tr_rtp) -> value rx_rtp {
|
|
/* increment counters */
|
|
if (rx_rtp.msg.rtp.payload_type != g_cfg.tx_payload_type) {
|
|
g_stats_rtp.num_pkts_rx_err_pt := g_stats_rtp.num_pkts_rx_err_pt+1;
|
|
}
|
|
g_stats_rtp.num_pkts_rx := g_stats_rtp.num_pkts_rx+1;
|
|
g_stats_rtp.bytes_payload_rx := g_stats_rtp.bytes_payload_rx +
|
|
lengthof(rx_rtp.msg.rtp.data);
|
|
if (ispresent(g_cfg.rx_fixed_payload) and rx_rtp.msg.rtp.data != g_cfg.rx_fixed_payload) {
|
|
g_stats_rtp.num_pkts_rx_err_payload := g_stats_rtp.num_pkts_rx_err_payload + 1;
|
|
}
|
|
if (g_cfg.iuup_mode) {
|
|
rx_rtp.msg.rtp.data := f_IuUP_Em_rx_decaps(g_iuup_ent, rx_rtp.msg.rtp.data);
|
|
}
|
|
}
|
|
[g_rx_enabled] RTCP.receive(tr_rtcp) -> value rx_rtp {
|
|
//log("RX RTCP: ", rx_rtp);
|
|
g_stats_rtcp.num_pkts_rx := g_stats_rtcp.num_pkts_rx+1;
|
|
}
|
|
|
|
/* transmit if timer has expired */
|
|
[g_tx_connected] T_transmit.timeout {
|
|
/* send one RTP frame, re-start timer */
|
|
f_tx_rtp(g_cfg.tx_fixed_payload);
|
|
T_transmit.start;
|
|
/* update counters */
|
|
g_stats_rtp.num_pkts_tx := g_stats_rtp.num_pkts_tx+1;
|
|
g_stats_rtp.bytes_payload_tx := g_stats_rtp.bytes_payload_tx +
|
|
lengthof(g_cfg.tx_fixed_payload);
|
|
}
|
|
|
|
/* fail on any unexpected messages */
|
|
[] RTP.receive {
|
|
setverdict(fail, "Received unexpected type from RTP");
|
|
mtc.stop;
|
|
}
|
|
[] RTCP.receive {
|
|
setverdict(fail, "Received unexpected type from RTCP");
|
|
mtc.stop;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|