As the title says, this PR adds RTP forwarders support to the SIP and NoSIP plug…ins. RTP forwarders are a very helpful feature that some plugins (VideoRoom and AudioBridge) supported already, and enabled a ton of interesting opportunities to sessions handled there: now this functionality is available to the SIP and NoSIP plugins too.
This was a sponsored development, which was paid for by our friends at [WebTrit](https://github.com/WebTrit) (who you may remember for sponsoring #3404 too, some time ago) and another company using Janus in production. Thank you guys, your support and interest in improving Janus is always appreciated!
If you're curious what RTP forwarders are about and why they may be useful to the SIP and NoSIP plugins too, it may be helpful to make a little step backward to descrive what RTP forwarders do in general. A more comprehensive introduction to RTP forwarders is provided in [this talk I made at FOSDEM](https://archive.fosdem.org/2020/schedule/event/janus/) a few years ago, but in a nutshell, as the name suggests, their purpose is to programmatically forward RTP packets externally. Within the context of the VideoRoom, for instance, this means intercepting the RTP packets a VideoRoom publisher is sending to Janus via WebRTC, and besides managing them via the VideoRoom SFU rules (e.g., relaying the media to interested subscribers), also forwarding the RTP traffic to an external UDP address configured via API. This allows external components and applications to receive media traffic originally sent via WebRTC, even without being on the WebRTC media path, which could be useful for different reasons: scalability (e.g., forwarding a VideoRoom publisher to multiple Streaming mountpoints on different instances, for a large scale one-to-many distribution), external recording, media processing (e.g., identify verification, transcriptions, audio/video manipulation, etc.), monitoring, lawful intercept, and so on and so forth. Long story short, this opens the door to a ton of interesting opportunities, since applications that may know nothing about WebRTC, but may be familiar with RTP, can now get access to WebRTC traffic for doing cool things with it. We personally use this feature a lot, as it's at the foundation of our scalable virtual event platform, our external videomixer, and our [Juturna-based](https://github.com/meetecho/juturna/) transcription service.
Which brings us to the SIP and NoSIP plugins. The companies that sponsored this effort were very interested in getting the same kind of externalised traffic out of SIP calls, but the fact RTP forwarders were only available in other plugins and not in the SIP/NoSIP ones made it impossible. This is what this PR addresses, in order to implement something like the following:
<img width="100%" alt="janus-sip-rf" src="https://github.com/user-attachments/assets/75a5ff2d-ea01-4c97-8a78-3e4a9c03f52f" />
As you can see from the diagram, the idea is that you still establish SIP calls via the SIP or NoSIP plugins as usual, but you can now programmatically tell the plugin to also extrnalise RTP packets (audio and/or video, coming from the WebRTC user and/or the SIP endpoint) to an external address as well. This externalised RTP traffic is created out of context, using the Janus API, and as such does _not_ involve SIP: the SIP signalling is actually completely unaware of that, since it's just forwarding rules that are added programmatically to the way the plugins route incoming RTP traffic. Where before we'd always only send incoming RTP traffic to the peer in the call, now we can also fork that media to one or more external addresses.
Using the API is not that dissimilar from how RTP forwarders work in the VideoRoom and AudioBridge plugin, with a key differentce: when in those plugins the request is synchronous, in the SIP and NoSIP plugin it's an asynchronous request that you perform on the handle that's handling the call you want the forwarding rules to work on.
To give some examples, this snippet only forwards audio and video coming from the WebRTC user, and not the SIP peer, to a remote address:
sipcall.send({message: {request: 'rtp_forward', streams: [{type: 'audio', host: '127.0.0.1', port: 5002}, {type: 'video', host: '127.0.0.1', port: 5004}]}})
since `audio` and `video` are the types we specify in the forwarders that we want to create. In this example, we're asking Janus to forward the audio packets the WebRTC user is sending in this SIP call to the `127.0.0.1:5002` address, and the video packets to `127.0.0.1:5004`.
To forward audio and video coming from the SIP peer, instead, we can use `peer_audio` and `peer_video` as types in the objects to create forwarders for, e.g.:
sipcall.send({message: {request: 'rtp_forward', streams: [{type: 'peer_audio', host: '127.0.0.1', port: 6002}, {type: 'peer_video', host: '127.0.0.1', port: 6004}]}})
Notice how the `rtp_forward` request expects an array of objects to signal which forwarders you want to create, and what for. This gives you flexibility to choose, in a drill down way, exactly what you want to forward at any given time. Should you want to forward both audio streams in a call, for instance, you can call `rtp_forward` with two objects where one type is `audio` and the other is `peer_audio`.
It's also worth pointing out that you can RTP-forward the same stream more than once. If you want the audio stream from the WebRTC user to be sent to multiple destinations, you can simply add multiple objects all with the same `type: "audio"` property and different target addresses.
A response to an `rtp_forward` request will return a unuque identifier for each forwarder that was created: you'll need that identifier for closing an RTP forwarder later on, or more in general keep track of it. You can also use the `listforwarders` request to obtain a list of all forwarders that are currently active on a call.
To get rid of an RTP forwarder, you can use the `stop_rtp_forward` like this:
sipcall.send({message: {request: 'stop_rtp_forward', stream_id: 3165041645}})
Notice that RTP forwarders can only be created on an active call (the request will fail if there's no call going on, since we wouldn't know which streams are currently active), and that RTP forwarders for a call are automatically destroyed as soon as the call is hung up.
It's worth highlighting how in all the requests we introduced above we're not providing info on which call we're trying to act upon. That is because the context of the call is implicit from the handle we send the request on. If the handle is currently used for a specific call, that specific call will be the target of RTP forwarding requests. In the future we may explore ways to also programmatically address other calls (e.g., via Call-ID or a different property), but at the moment this implicit addressing already covers the requirement (especially considering we currently don't have unique ways of addressing calls in the SIP and NoSIP plugins).
That said, the syntax is exactly the same for both SIP and NoSIP plugins. While the syntax is straightforward enough to understand, the PR also contains doxygen code to generate additional documentation in both plugins for this new functionality.
Feedback welcome! We plan to merge this relatively soon, so make sure you test this thoroughly. While this is currently a PR for `master`, we'll backport this to `0.x` as well after it's merged.