[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