Create router with two audio/opus media codecs option

I am trying to create a router with 2 media codecs as follows:

const CODEC_OPTIONS = [{
  kind: ‘audio’,
  mimeType: ‘audio/opus’,
  clockRate: 48000,
  channels: 2,
  preferredPayloadType: 109,
  },
  {
  kind: ‘audio’,
  mimeType: ‘audio/opus’,
  clockRate: 48000,
  channels: 2,
  preferredPayloadType: 111,
}];

Chrome offers 111 while Firefox uses 109. When I call WebRtcTransport.produce/WebRtcTransport.consume , mediasoup always picks the first codec. I traced the code and fixed the issue as follow (see the last if statement):

node_modules/mediasoup/node/lib/ortc.js:807
function matchCodecs(aCodec, bCodec, { strict = false, modify = false } = {}) {
    const aMimeType = aCodec.mimeType.toLowerCase();
    const bMimeType = bCodec.mimeType.toLowerCase();
    if (aMimeType !== bMimeType)
        return false;
    if (aCodec.clockRate !== bCodec.clockRate)
        return false;
    if (aCodec.channels !== bCodec.channels)
        return false;

    // I added this if statement, it doesn't exist originaly
    if (aCodec.preferredPayloadType && bCodec.preferredPayloadType && aCodec.preferredPayloadType !== bCodec.preferredPayloadType)
        return false;
    ...
}

I was wondering what is the right way to do this without modifying mediasoup source code

Which issue do you mean? The problem here is that you think that codec payload in Router codecs must be the same as the one chosen by browsers. Nowhere in the mediasoup docs you’ll read anything like that. So please don’t try to fix issues that do not exist.

Why would you need this? If you are not aiming at something extraordinary, just remove preferredPayloadType from the options at all (along with the second instance of the codec).

I am using client similar the following client, when running it using chrome it works with preferredPayloadType = 111, and it only works for firefox when change the preferredPayloadType to be 109, otherwise I can’t hear any voice


const API_IP_PORT = "127.0.0.1:8080";

/* eslint-disable  */

function addVideoForStream(stream, local) {
    //Create new video element
    const video = document.querySelector (local ? "#local" : "#remote");
    //Set same id
    video.streamid = stream.id;
    //Set src stream
    video.srcObject = stream;
    //Set other properties
    video.autoplay = true;
    video.muted = local;
}

let lastPackets = 0;
let lastBytes = 0;
function updateIncomingStats(statsResult) {
    const jitter = document.querySelector("#jitter");
    jitter.innerText = `Jitter: ${statsResult.jitter}`;

    const packetsLost = document.querySelector("#packetsLost");
    packetsLost.innerText = `Packets Lost: ${statsResult.packetsLost}`;

    const packetsReceived = document.querySelector("#packetsReceived");
    packetsReceived.innerText = `Packets Received: ${statsResult.packetsReceived}`;

    const packetsPerSecond = document.querySelector("#packetsPerSecond");
    packetsPerSecond.innerText = `Packets per second: ${statsResult.packetsReceived - lastPackets}`;
    lastPackets = statsResult.packetsReceived;

    const bytesPerSecond = document.querySelector("#bytesPerSecond");
    bytesPerSecond.innerText = `Bytes per second: ${statsResult.bytesReceived - lastBytes}`;
    lastBytes = statsResult.bytesReceived;

    const allStates = document.querySelector("#allStates");
    let str = '';
    for (const k in statsResult) {
        str = `${str}<b>${k}:</b> ${statsResult[k]}<br />`;
    }
    allStates.innerHTML = `<h2>AllStates:</h2> ${str}`;
    lastBytes = statsResult.bytesReceived;
}

function httpAsync(type, theUrl, data, callback) {
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function() {
        if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
            callback(xmlHttp.responseText);
    };
    xmlHttp.open(type, theUrl, true); // true for asynchronous 
    xmlHttp.setRequestHeader('Content-Type', 'application/json');
    xmlHttp.send(data);
}


//Create PC
const localPc = new RTCPeerConnection();

navigator.mediaDevices.getUserMedia({
    audio: true,
    video: false
})
.then(function(stream) {
    console.debug("getUserMedia success",stream);
    
    // Play it
    addVideoForStream(stream,true);

    // Add stream to peer connection
    localPc.addStream(stream);
    
    // Create new offer
    return localPc.createOffer({
        offerToSendAudio: true,
        offerToSendVideo: false,
        offerToReceiveAudio: false,
        offerToReceiveVideo: false
    });
})
.then(function(offer){
    console.debug("createOffer success",offer);
    //We have sdp
    const sdp = offer.sdp;
    //Set it
    localPc.setLocalDescription(offer);
    console.log('Local: ' + sdp);
    
    const data = {
        sdp,
        recordAudio: false,
    };

    httpAsync('POST', `http://${API_IP_PORT}/producer`, JSON.stringify(data), (response) => {
        const parsedObj = JSON.parse(response);
        const sdp = parsedObj.sdp;
        
        console.log('Remote: ' + sdp);
        localPc.setRemoteDescription(new RTCSessionDescription({
            type:'answer',
            sdp: sdp
        }), function () {
            console.log(`JOINED ${parsedObj.uuid}`);

            // remoteConnectionStart(parsedObj.uuid);
            remoteConnectionStart(parsedObj.uuid);
        }, function (err) {
            console.error("Error joining",err);
        });
    });
})
.catch(function(error){
    console.error("Error",error);
});

let remotePc;
let data;
const producerUuidInput = document.getElementById('producer-uuid');
const remoteConnectionStart = (uuid) => {
    console.log("Attempting to link up to uuid: " + uuid);

    remotePc = new RTCPeerConnection({
        bundlePolicy: "max-bundle",
        rtcpMuxPolicy : "require"
    });

    remotePc.onaddstream = function(event) {
        console.debug("onAddStream",event);
        
        addVideoForStream(event.stream, false);
        setInterval(async function(){
           const results = await remotePc.getStats();
           for (let result of results.values()) {
               if (result.type === "inbound-rtp") {
                   updateIncomingStats(result);
               }
           }
        }, 1000);
    };

    remotePc.createOffer({
        offerToReceiveVideo: false,
        offerToReceiveAudio: true,
        offerToSendAudio: false,
        offerToSendVideo: false
    }).then(offer => {
        data = {
            uuid: uuid,
            sdp: offer.sdp
        };
        console.log('Local SDP for recv' + offer.sdp);
        remotePc.setLocalDescription(offer);

        callConsumer();
    });
};

const callConsumer = () => {
    if (producerUuidInput.value) {
        data.uuid = producerUuidInput.value;
    }
    httpAsync('POST', `http://${API_IP_PORT}/consumer`, JSON.stringify(data), (response) => {
        const parsedObj = JSON.parse(response);
        const sdp = parsedObj.sdp;
        console.log('Remote SDP for recv' + sdp);
        
        remotePc.setRemoteDescription(new RTCSessionDescription({
                type:'answer',
                sdp: sdp
            }), function () {
                console.log("JOINED");
                
                // setTimeout(() => {
                //     console.log('Stopping remote listen');
                //     localPc.close();
                // }, 30 * 1000);
                
            }, function (err) {
                console.error("Error joining",err);
                console.log(sdp);
            }
        );
    });
}

what do you think I should do to fix the issue?

I tried removing it but couldn’t hear any voice, do have any idea why?

Make sure that peer connection is established and well. Check chrome://webrtc-internals / about:webrtc on the client side.

connection is established, but can’t hear any voice

If statistics shows that there is some incoming data matching the amount on the other end, then something is wrong with the player. Browsers nowadays may require some manual action from the user to unmute the sound on the page: a click on a button or another element, for example.

For whatever reason you are not using mediasoup-client but you are creating your own PeerConnection. Ok, things won’t work. So simple question: have you read mediasoup documentation, specially the section about communicating client and server, yes or not?

@ibc yes, I did read the documentation many times, but it doesn’t help in my case because I’m not using mediasoup-client, I have no control to change the client. I only receive an SDP offer and I’m supposed to answer with an SDP answer. I got Mediasoup to work finally by using two Routers (one of them configured on payloadType 111 while the other 109).

If you use 2 PeerConnections, one for sending and one for receiving as we do in mediasoup-client, then you wouldn’t need to use N routers to deal with different default payload types in each browser.

@ibc in the client snippet code above, I am using 2 RTCPeerConnection and it is not working, could you check it and tell me what am I missing?

I cannot explain how to replicate every trick and complex logic that we run in mediasoup-client internals. But when you use a PC for sending you must create the SDP answer with payload types matching the ones generated by the browser in its SDP offer. Then there is RTCT feedback negotiation, RTP extensions negotiation, etc etc etc.

The code doesn’t even call createAnswer, how is it supposed to work?

I’ve been using this client for testing with mediasoup on chrome, it creates a producer and a consumer and I can hear myself, when using FF is doesn’t work below is SDP

Producer Part:
client SDP

v=0
o=mozilla...THIS_IS_SDPARTA-99.0 1851692998368445695 0 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 8D:A8:C8:33:9C:07:1F:B4:48:DE:1D:2A:BE:7B:EE:20:C0:7A:0A:7D:32:F4:EF:4F:CA:3B:A7:40:CF:1D:E2:2F
a=group:BUNDLE 0
a=ice-options:trickle
a=msid-semantic:WMS *
m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101
c=IN IP4 0.0.0.0
a=sendonly
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level
a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid
a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1
a=fmtp:101 0-15
a=ice-pwd:1c626f42fa98700690bf7d41dfdf2d24
a=ice-ufrag:1c710932
a=mid:0
a=msid:{635563b2-ec36-4c99-a708-4e9a08eba506} {7966a53d-639c-456a-9a71-5a5b9563f1d2}
a=rtcp-mux
a=rtpmap:109 opus/48000/2
a=rtpmap:9 G722/8000/1
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000/1
a=setup:actpass
a=ssrc:889498617 cname:{929121d7-4871-4278-83c6-322fdbb8d2c9}

Mediasoup SDP answer

v=0
o=- 1677162821816 1 IN IP4 127.0.0.1
s=semantic-sdp
c=IN IP4 0.0.0.0
t=0 0
a=ice-lite
a=msid-semantic: WMS *
a=group:BUNDLE 0
m=audio 9 UDP/TLS/RTP/SAVPF 109
a=rtpmap:109 opus/48000/2
a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1;usedtx=1
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap-allow-mixed
a=setup:passive
a=mid:0
a=recvonly
a=ice-ufrag:3uopz5f2ns5okof9snl2wu0zkdahpl8u
a=ice-pwd:l8yfhrdgtuv2ua5n6hnkmg2h2nwnzv24
a=fingerprint:sha-256 32:F1:6B:DB:DC:4E:5C:EE:C8:76:3A:8E:CB:59:A0:03:89:3F:A3:FB:87:46:14:7D:9F:31:24:5D:0D:75:17:A5
a=candidate:1 1 udp 1076558079 192.168.1.100 4210 typ host
a=rtcp-mux
a=rtcp-rsize

Consumer Part
Client SDP

recvv=0
o=mozilla...THIS_IS_SDPARTA-99.0 8400218602549350948 0 IN IP4 0.0.0.0
s=-
t=0 0
a=fingerprint:sha-256 BD:C3:B0:76:DE:72:89:73:5A:0C:DD:4E:4E:BB:DB:A3:86:A7:B3:F4:0E:77:A0:81:A2:EA:CA:53:44:F6:96:5E
a=group:BUNDLE 0
a=ice-options:trickle
a=msid-semantic:WMS *
m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101
c=IN IP4 0.0.0.0
a=recvonly
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level
a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid
a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1
a=fmtp:101 0-15
a=ice-pwd:e3a8af1b5372feebe6e6d5a417efc582
a=ice-ufrag:ec4fd345
a=mid:0
a=rtcp-mux
a=rtpmap:109 opus/48000/2
a=rtpmap:9 G722/8000/1
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:101 telephone-event/8000/1
a=setup:actpass
a=ssrc:801915867 cname:{4a5ab449-ad7f-4d1b-90fc-7e27d3581de4}

Mediasoup SDP answer

recvv=0
o=- 1677162821878 1 IN IP4 127.0.0.1
s=semantic-sdp
c=IN IP4 0.0.0.0
t=0 0
a=ice-lite
a=msid-semantic: WMS *
a=group:BUNDLE 0
m=audio 9 UDP/TLS/RTP/SAVPF 109
a=rtpmap:109 opus/48000/2
a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1;usedtx=1
a=extmap-allow-mixed
a=setup:passive
a=mid:0
a=sendonly
a=ice-ufrag:aklnlbyjoxcsojpwjpmoukjo5ylhtcuh
a=ice-pwd:4er9zl89ezeu6tbutvcic69ivcnrf24f
a=fingerprint:sha-256 32:F1:6B:DB:DC:4E:5C:EE:C8:76:3A:8E:CB:59:A0:03:89:3F:A3:FB:87:46:14:7D:9F:31:24:5D:0D:75:17:A5
a=candidate:1 1 udp 1076558079 192.168.1.100 16426 typ host
a=ssrc:817063224 cname:0d1137ac
a=ssrc:817063224 msid:0d1137ac audio1
a=rtcp-mux
a=rtcp-rsize