diff --git a/lib/tasks/dial.js b/lib/tasks/dial.js index 1a04dc7d..218ba718 100644 --- a/lib/tasks/dial.js +++ b/lib/tasks/dial.js @@ -17,7 +17,7 @@ const dbUtils = require('../utils/db-utils'); const debug = require('debug')('jambonz:feature-server'); const {parseUri} = require('drachtio-srf'); const {ANCHOR_MEDIA_ALWAYS, JAMBONZ_DISABLE_DIAL_PAI_HEADER} = require('../config'); -const { isOnhold } = require('../utils/sdp-utils'); +const { isOnhold, isOpusFirst } = require('../utils/sdp-utils'); const { normalizeJambones } = require('@jambonz/verb-specifications'); function parseDtmfOptions(logger, dtmfCapture) { @@ -488,7 +488,8 @@ class TaskDial extends Task { headers: this.headers, proxy: `sip:${sbcAddress}`, callingNumber: this.callerId || req.callingNumber, - ...(this.callerName && {callingName: this.callerName}) + ...(this.callerName && {callingName: this.callerName}), + opusFirst: isOpusFirst(this.cs.ep.remote.sdp) }; const t = this.target.find((t) => t.type === 'teams'); @@ -790,9 +791,11 @@ class TaskDial extends Task { assert(cs.ep && sd.ep); try { + // Wait until we got new SDP from B leg to ofter to A Leg const aLegSdp = cs.ep.remote.sdp; + await sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp); const bLegSdp = sd.dlg.remote.sdp; - await Promise.all[sd.releaseMediaToSBC(aLegSdp, cs.ep.local.sdp), cs.releaseMediaToSBC(bLegSdp)]; + await cs.releaseMediaToSBC(bLegSdp); this.epOther = null; this.logger.info('Dial:_releaseMedia - successfully released media from freewitch'); } catch (err) { diff --git a/lib/utils/place-outdial.js b/lib/utils/place-outdial.js index 28e1eac5..823704e5 100644 --- a/lib/utils/place-outdial.js +++ b/lib/utils/place-outdial.js @@ -15,6 +15,7 @@ const RootSpan = require('./call-tracer'); const uuidv4 = require('uuid-random'); const HttpRequestor = require('./http-requestor'); const WsRequestor = require('./ws-requestor'); +const {makeOpusFirst} = require('./sdp-utils'); class SingleDialer extends Emitter { constructor({logger, sbcAddress, target, opts, application, callInfo, accountInfo, rootSpan, startSpan, dialTask}) { @@ -151,7 +152,7 @@ class SingleDialer extends Emitter { Object.assign(opts, { proxy: `sip:${this.sbcAddress}`, - localSdp: this.ep.local.sdp + localSdp: opts.opusFirst ? makeOpusFirst(this.ep.local.sdp) : this.ep.local.sdp }); if (this.target.auth) opts.auth = this.target.auth; inviteSpan = this.startSpan('invite', { diff --git a/lib/utils/sdp-utils.js b/lib/utils/sdp-utils.js index 4a0ccb42..0b7e8cc7 100644 --- a/lib/utils/sdp-utils.js +++ b/lib/utils/sdp-utils.js @@ -12,6 +12,30 @@ const mergeSdpMedia = (sdp1, sdp2) => { return sdpTransform.write(parsedSdp1); }; +const getCodecPlacement = (parsedSdp, codec) => parsedSdp?.media[0]?.rtp?.findIndex((e) => e.codec === codec); + +const isOpusFirst = (sdp) => { + return getCodecPlacement(sdpTransform.parse(sdp), 'opus') === 0; +}; + +const makeOpusFirst = (sdp) => { + const parsedSdp = sdpTransform.parse(sdp); + // Find the index of the OPUS codec + const opusIndex = getCodecPlacement(parsedSdp, 'opus'); + + // Move OPUS codec to the beginning + if (opusIndex > 0) { + const opusEntry = parsedSdp.media[0].rtp.splice(opusIndex, 1)[0]; + parsedSdp.media[0].rtp.unshift(opusEntry); + + // Also move the corresponding payload type in the "m" line + const opusPayloadType = parsedSdp.media[0].payloads.split(' ')[opusIndex]; + const otherPayloadTypes = parsedSdp.media[0].payloads.split(' ').filter((pt) => pt != opusPayloadType); + parsedSdp.media[0].payloads = [opusPayloadType, ...otherPayloadTypes].join(' '); + } + return sdpTransform.write(parsedSdp); +}; + const extractSdpMedia = (sdp) => { const parsedSdp1 = sdpTransform.parse(sdp); if (parsedSdp1.media.length > 1) { @@ -28,5 +52,7 @@ const extractSdpMedia = (sdp) => { module.exports = { isOnhold, mergeSdpMedia, - extractSdpMedia + extractSdpMedia, + isOpusFirst, + makeOpusFirst }; diff --git a/test/index.js b/test/index.js index ed9322b1..fa898493 100644 --- a/test/index.js +++ b/test/index.js @@ -17,5 +17,6 @@ require('./config-test'); require('./queue-test'); require('./in-dialog-test'); require('./http-proxy-test'); +require('./sdp-utils-test'); require('./remove-test-db'); require('./docker_stop'); \ No newline at end of file diff --git a/test/sdp-utils-test.js b/test/sdp-utils-test.js new file mode 100644 index 00000000..169547f4 --- /dev/null +++ b/test/sdp-utils-test.js @@ -0,0 +1,26 @@ +const test = require('tape'); +const {makeOpusFirst, isOpusFirst} = require('../lib/utils/sdp-utils'); +const sdpTransform = require('sdp-transform'); + +test('test opus first', (t) => { + const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n'; + const opusSdp = makeOpusFirst(sdp); + const parsedSdp = sdpTransform.parse(opusSdp); + const opusIndex = parsedSdp.media[0].rtp.findIndex((entry) => entry.codec === 'opus'); + t.ok(opusIndex === 0, 'succesffuly move opus to be first offer') + t.end(); +}); + + +test('test is opus first', (t) => { + + const sdp = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n'; + t.ok(isOpusFirst(sdp), "opus is first offer"); + + const sdp2 = 'v=0\r\no=- 3348584794228993675 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS caca8b77-5ae5-4e73-a4d5-de1fce930335\r\nm=audio 57088 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\nc=IN IP4 14.238.89.50\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:1401281302 1 udp 2122260223 10.231.36.146 57088 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2173263513 1 udp 1686052607 14.238.89.50 57088 typ srflx raddr 10.231.36.146 rport 57088 generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:k5nc\r\na=ice-pwd:J0qwMs6HrIcFNZbDG5m8Kqpk\r\na=ice-options:trickle\r\na=fingerprint:sha-256 66:DE:9A:76:CE:11:2D:65:C4:08:C7:87:B4:90:7E:F1:8D:07:B9:F4:FF:E3:81:D7:E7:7D:C6:56:47:01:6E:55\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=sendrecv\r\na=msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\na=rtcp-mux\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 minptime=10;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:9 G722/8000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:111 opus/48000/2\r\na=rtpmap:110 telephone-event/48000\r\na=rtpmap:126 telephone-event/8000\r\na=ssrc:3207459321 cname:4nyPJ6KXvseBUIhu\r\na=ssrc:3207459321 msid:caca8b77-5ae5-4e73-a4d5-de1fce930335 52ad01f1-b1df-4b8e-a208-9201e98b6f7b\r\n'; + t.ok(!isOpusFirst(sdp2), "opus is not first offer") + + const sdp3 = 'v=0\r\no=xhoaluu2 1314 1504 IN IP4 192.168.1.4\r\ns=Talk\r\nc=IN IP4 192.168.1.4\r\nt=0 0\r\na=ice-pwd:397d063ea23fdc05164e3ee4\r\na=ice-ufrag:16c449a3\r\na=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics\r\na=group:BUNDLE as\r\na=record:off\r\nm=audio 56542 RTP/AVPF 0 8\r\nc=IN IP4 14.226.233.151\r\na=rtcp-mux\r\na=mid:as\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=rtcp:63076 IN IP4 192.168.1.4\r\na=candidate:1 1 UDP 2130706303 192.168.1.4 56542 typ host\r\na=candidate:1 2 UDP 2130706302 192.168.1.4 63076 typ host\r\na=candidate:2 1 UDP 2130706431 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 56542 typ host\r\na=candidate:2 2 UDP 2130706430 2001:ee0:d744:dcf0:c1d3:d73f:7a93:dc9f 63076 typ host\r\na=candidate:3 1 UDP 2130706431 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 56542 typ host\r\na=candidate:3 2 UDP 2130706430 2001:ee0:d744:dcf0:15:6be3:8e6b:b736 63076 typ host\r\na=candidate:4 1 UDP 1694498687 14.226.233.151 56542 typ srflx raddr 192.168.1.4 rport 56542\r\na=rtcp-fb:* trr-int 1000\r\na=rtcp-fb:* ccm tmmbr'; + t.ok(!isOpusFirst(sdp2), "opus is not first offer") + t.end(); +}); \ No newline at end of file