[OpenSIPS-Users] SIPREC video calls
Walter Schober
walter.schober at neotel.at
Thu Feb 19 14:21:44 UTC 2026
My final solution (working with video and WebRTC a=mid:X markers:
Opensips Recording HOP:
route[setup_rec] {
# https://www.opensips.org/Documentation/Tutorials-SIPREC-2-4
# remove mid:0 - https://support.sipwise.com/view.php?id=64475 - offer/answer w/ same mid:0 mixes up labels and returns single m= line
# temporary until fixed in RTPEngine
$rtp_relay_ctx(flags) = "sdp-attr-remove-audio-mid sdp-attr-remove-video-mid sdp-attr-remove-audio-msid sdp-attr-remove-video-msid sdp-attr-remove-none-msid-semantic";
rtp_relay_engage("rtpengine");
$avp(x-system) = $(ru{uri.param,x-system}); # without a x-system we do have a problem here. It MUST be present
t_on_reply("setup_rec");
}
onreply_route[setup_rec] {
if ($rs=="200") {
# otherwise the recording would start right after the first 18x with SDP
xlog("L_INFO", "Start recording on 200 OK for ctx(callid)=$rtp_relay_ctx(callid)");
# https://opensips.org/docs/modules/3.6.x/siprec.html#func_siprec_start_recording
$siprec(headers) = "X-Call-ID: "+$ci+"\r\n";
# the defaults do just fine. Otherwise we would need to store the values from INVITE request to have them here on the reply
# $xml(caller_xml) = "<nameID></nameID>";
# $xml(caller_xml/nameID.attr/aor) = $fU+"@"+$fd"; # remove any sip:, or ports
# if ($(fn{s.len})) $xml(caller_xml/nameID) = "<name>"+$fn+"</name>";
# $siprec(caller) = $xml(caller_xml/nameID);
#
# $xml(callee_xml) = "<nameID></nameID>";
# $xml(callee_xml/nameID.attr/aor) = $tU+"@"+$td"; # remove any sip:, or ports
# #$xml(callee_xml/nameID) = "<name></name>";
# $siprec(callee) = $xml(callee_xml/nameID);
$siprec(from_uri) = $fu;
$siprec(to_uri) = $tu;
# not only IP address but also RTPEngine flags can be set for the SRS leg in
# sdp-media-remove=video sends "sdp-media-remove": "video" instead of "sdp-media-remove": [ "video" ] -> video is not removed - see Conclusion
# Alternative solution in rtpengine.conf:
# [templates]
# SIPREC = sdp-media-remove=[video text image]
# RemMid = sdp-attr-remove-audio-mid sdp-attr-remove-video-mid sdp-attr-remove-audio-msid sdp-attr-remove-video-msid sdp-attr-remove-none-msid-semantic
# subscribe request = sdp-media-remove=[video text image]
# subscribe answer = allow-transcoding asymmetric
# either use directly:
$siprec(media) = "allow-transcoding asymmetric"; # -> sdp-media-remove does not send ARRAY
# or use a template:
#$siprec(media) = "template=SIPREC"; # -> sdp-media-remove=[video text image] is ignored, but other parameters do work!
# or use the default template for "subscribe request" and "subscribe answer"
# -> still the sdp-media-remove does not work
# Conclusion:
# - do directly to avoid dependency in rtpengine.conf
# - sdp-media-remove would work for the „offer-style" request, but not for the „offer-style" reply, so it is not done on the subscribe request reply!
# - rework the SDP body on the next-hop Hydra that has to manipulate the XMS anyway
siprec_start_recording("sip:opensips-srs at 192.168.48.161:5060;x-system=$avp(x-system), sip:opensips-srs at 192.168.48.162:5060;x-system=$avp(x-system)");
}
}
Kamailio (sorry guys, but had it ready with xmlops although XML module looks good) next hop to SRS:
modparam("xmlops", "xml_ns", "ac=urn:ietf:params:xml:ns:recording")
modparam("xmlops", "xml_ns", "os=urn:ietf:params:xml:ns:recording:1")
# -----------------------------------------------------------------------------------
# Forward PCR INVITE to correct SRS
# Convert OpenSIPS XML Format to Audiocodes Format understood by our SRS
# -----------------------------------------------------------------------------------
route[PCR_OPENSIPS] {
$var(boundary) = $(cT{param.value,boundary});
get_body_part('application/sdp', "$var(SDP)");
get_body_part('application/rs-metadata+xml', "$avp(X)");
# do the changes ...
avp_subst("$avp(X)", "/^Content-Disposition:.+//"); # why is this in the variable?
avp_subst("$avp(X)", "/^\s+//g"); # remove any Spaces and Tabs
avp_subst("$avp(X)", "/^\t/ /g"); # remove any empty lines - important for $xml(rec=>doc)
xlog("L_DEBUG", ">>> INPUT: $avp(X)");
$xml(rec=>doc) = $avp(X); # first line MUST NOT be an empty line!
# https://www.kamailio.org/docs/modules/devel/modules/xmlops.html
# https://grantm.github.io/perl-libxml-by-example/xpath.html
$var(groupId) = $xml(rec=>xpath:/os:recording/os:group/@group_id);
$var(sessionId) = $xml(rec=>xpath:/os:recording/os:session/@session_id);
$var(sipSession) = $xml(rec=>xpath:/os:recording/os:session/os:sipSessionID);
$var(stime) = $xml(rec=>xpath:/os:recording/os:sessionrecordingassoc[@session_id="$var(sessionId)"]/os:associate-time);
xlog("L_INFO", ">> ========== groupId=$var(groupId) sessionId=$var(sessionId) time=$var(stime)");
$var(out) = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<recording xmlns=\"urn:ietf:params:xml:ns:recording\">\r\n<datamode>complete</datamode>\r\n<group id=\""+$var(groupId)+"\">\r\n"+$var(stime)+"\r\n</group>\r\n"+
"<session id=\""+$var(sessionId)+"\">\r\n"+
"<group-ref>"+$var(groupId)+"</group-ref>\r\n"+
$var(sipSession)+"\r\n"+
$var(stime)+"\r\n</session>\r\n";
xlog("L_INFO", ">> ========== partIDs: $xml(rec=>xpath:/os:recording/os:participant/@participant_id)");
$var(str) = $xml(rec=>xpath:/os:recording/os:participant/@participant_id);
$var(i) = 0;
while ($(var(str){s.select,$var(i),,}{s.len})) {
xlog("L_DEBUG", ">>> $var(i) - $var(str)");
$var(id) = $(var(str){s.select,$var(i),,});
$var(nameId) = $xml(rec=>xpath:/os:recording/os:participant[@participant_id="$var(id)"]/os:nameID);
$var(name) = $xml(rec=>xpath:/os:recording/os:participant[@participant_id="$var(id)"]/os:nameID/os:name);
$var(time) = $xml(rec=>xpath:/os:recording/os:participantsessionassoc[@participant_id="$var(id)"]/os:associate-time);
$var(send) = $xml(rec=>xpath:/os:recording/os:participantstreamassoc[@participant_id="$var(id)"]/os:send);
$var(recv) = $xml(rec=>xpath:/os:recording/os:participantstreamassoc[@participant_id="$var(id)"]/os:recv);
xlog("L_DEBUG", ">>> PartID($var(i)): $var(id) - $var(nameId) - $var(time) - $var(send) - $var(recv)");
$var(out) = $var(out)+'<participant id="'+$var(id)+'" session="'+$var(sessionId)+"\">\r\n"+$var(nameId)+"\r\n"+$var(time)+"\r\n"+$var(send)+"\r\n"+$var(recv)+"\r\n</participant>\r\n";
$var(i) = $var(i) + 1;
}
xlog("L_INFO", ">> ========== streamIDs: $xml(rec=>xpath:/os:recording/os:stream/@stream_id)");
$var(label1) = "";
$var(label2) = "";
if (sdp_with_media("video")) {
$var(in_audio) = 0;
sdp_iterator_start("s1");
while (sdp_iterator_next("s1")) {
# detect start of any media section
if ($sdpitval(s1) =~ "^m=") {
if ($sdpitval(s1) =~ "^m=audio[ \t]") {
$var(in_audio) = 1;
} else {
$var(in_audio) = 0;
}
} else {
# only evaluate attributes if we are inside audio section
if ($var(in_audio) == 1) {
if ($sdpitval(s1) =~ "^a=label:") {
$var(lbl) = $(sdpitval(s1){s.trim}{re.subst,/^a=label:(.+)/\1/});
xlog("L_DEBUG", ">> found audio label: $var(lbl)");
if ($var(label1) == "") {
$var(label1) = $var(lbl);
} else if ($var(label2) == "") {
$var(label2) = $var(lbl);
} else {
xlog("L_INFO", ">> found more than 2 labels! Ignoring!");
}
}
}
}
}
sdp_iterator_end("s1");
}
$var(str) = $xml(rec=>xpath:/os:recording/os:stream/@stream_id);
$var(i) = 0;
while ($(var(str){s.select,$var(i),,}{s.len})) {
xlog("L_DEBUG", ">>> $var(i) - $var(str)");
$var(id) = $(var(str){s.select,$var(i),,});
$var(label) = $xml(rec=>xpath:/os:recording/os:stream[@stream_id="$var(id)"]/os:label/text());
xlog("L_DEBUG", ">>> StreamID($var(i)): $var(id) - $var(label)");
if ($var(i) == 0 && $var(label1) != "" && $var(label1) != $var(label)) {
xlog("L_INFO", ">> stream label not an audio label. replace $var(label) -> $var(label1)");
$var(label) = $var(label1);
} else if ($var(i) == 1 && $var(label2) != "" && $var(label2) != $var(label)) {
xlog("L_INFO", ">> stream label not an audio label. replace $var(label) -> $var(label2)");
$var(label) = $var(label2);
}
$var(out) = $var(out)+'<stream id="'+$var(id)+'" session="'+$var(sessionId)+"\">\r\n<label>"+$var(label)+"</label>\r\n</stream>\r\n";
$var(i) = $var(i) + 1;
}
$var(out) = $var(out) + '</recording>';
xlog("L_DEBUG", ">>> OUT $var(out)");
# Content-Type Header is not adopted here
set_body_multipart("$var(SDP)", "application/sdp", "$var(boundary)"); # default delimiter unique-boundary-1
msg_apply_changes();
append_body_part("$var(out)", "application/rs-metadata+xml", "recording-session");
xlog("L_DEBUG", ">>> found first media IP: $sdp(c:ip)");
$var(cFound) = 0;
sdp_iterator_start("s1");
while(sdp_iterator_next("s1")) {
xlog("L_DEBUG", ">> found=$var(cFound) body line: $sdpitval(s1)");
if ($sdpitval(s1) =~ "^c=IN IP4 ") {
xlog("L_INFO", ">> c-line found before m-line!");
$var(cFound) = 1;
break;
}
if ($sdpitval(s1) =~ "^m=audio [0-9]+ RTP" && $var(cFound) == 0) {
xlog("L_INFO", ">> no session level c-line, inserting one");
sdp_iterator_insert("s1", "c=IN IP4 $sdp(c:ip)\r\n");
break;
}
}
sdp_iterator_end("s1");
add_rr_param(";siprec=os");
}
route[PCR]
{
if ($rU=="opensips-srs" && uri==myself) {
if (is_method("CANCEL")) {
if (!t_check_trans()) {
# we fwd the cancel anyway, restart of kamailio during transaction would cause problems on cancel otherwise
xlog("L_ERR", "CANCEL w/o matching transaction");
}
t_relay();
exit;
}
if (is_method("INVITE") && has_body("multipart/mixed")) {
route(PCR_OPENSIPS);
}
if (sdp_with_media("video")) {
# PCR_OPENSIPS replaces the labels from video to audio, PCR server strips it anyway
sdp_remove_media("video");
xlog("L_INFO", "Removed video from SRS call");
}
$rU = "c5-srs"; # continue below with Audiocodes Style Request Handling
}
…
Might it help someone ….
Br Walter
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.opensips.org/pipermail/users/attachments/20260219/ea0de669/attachment-0001.html>
More information about the Users
mailing list