No audio in WebRTC call via ARI bridge (Asterisk 20.15.2)

Hi everyone,
I’m trying to connect a browser-based agent to callers through an ARI bridge, but I’m stuck with no audio in either direction. I’d really appreciate any guidance.

Environment

    • Asterisk: 20.15.2 (built from source)

    • Frontend: React + SIP.js (WebRTC)

    • Signaling: WS → Asterisk (ws://127.0.0.1:8088/ws)

    • Bridge: mixing bridge created via ARI

    • Certificates auto-generated by Asterisk (dtls_auto_generate_cert=yes)

      [transport-webrtc]
      type=transport
      protocol=wss
      bind=0.0.0.0:8088

      [agent-auth]
      type=auth
      auth_type=userpass
      username=agent-webrtc
      password=StrongSecret

      [agent-webrtc]
      type=endpoint
      transport=transport-webrtc
      context=agent
      disallow=all
      allow=opus,ulaw,alaw,g729,g722,gsm
      dtmf_mode=rfc4733
      trust_id_inbound=yes
      send_connected_line=no
      direct_media=no
      rtp_symmetric=yes
      force_rport=yes
      rewrite_contact=yes
      ice_support=yes
      media_encryption=dtls
      use_avpf=yes
      rtcp_mux=yes
      bundle=yes
      dtls_verify=fingerprint
      dtls_setup=actpass
      dtls_auto_generate_cert=yes
      webrtc=yes
      use_ptime=yes
      aors=agent-webrtc
      auth=agent-auth
      preferred_codec_only=no
      timers=no
      timers_sess_expires=1800
      timers_min_se=90
      identify_by=auth_username,username,ip

      [agent-webrtc]
      type=aor
      max_contacts=1
      async function handleAgentJoinCall(callId, bridgeId, agentId, userId) {
      try {
      console.log(Agent ${agentId} joining call ${callId} on bridge ${bridgeId});

      const callInfo = await callDatabaseService.getCallFromDatabase(callId);
      if (!callInfo) throw new Error(`Call ${callId} not found`);
      
      const bridge = await clientRef.bridges.get({ bridgeId });
      if (!bridge.channels || bridge.channels.length === 0) {
        throw new Error(`Call ${callId} ended – bridge has no channels`);
      }
      console.log(`✅ Bridge ${bridgeId} has ${bridge.channels.length} channels`);
      
      const agentChannel = await clientRef.channels.originate({
        endpoint: `PJSIP/agent-webrtc`,
        app: ARI_CONFIG.appName,
        appArgs: `${callId},${bridgeId},${agentId}`,
        callerId: `Agent-${agentId}`,
        timeout: 30,
        variables: {
          CALL_ID: callId,
          BRIDGE_ID: bridgeId,
          AGENT_ID: agentId,
          USER_ID: userId,
        },
      });
      console.log(`✅ Agent channel created: ${agentChannel.id}`);
      
      await callDatabaseService.updateCallStatus(callId, {
        metadata: {
          ...callInfo.metadata,
          agentJoined: true,
          agentId,
          agentChannelId: agentChannel.id,
          agentJoinTime: new Date().toISOString(),
          waitingForAgent: false,
        },
      });
      
      return { success: true, agentChannelId: agentChannel.id };
      

      } catch (err) {
      console.error(“:cross_mark: Error joining agent:”, err);
      throw err;
      }
      }

      function registerEventHandlers(client) {
      client.on(“StasisStart”, async (event, channel) => {
      const context = event.channel?.dialplan?.context;
      const [callIdArg, bridgeIdArg] = event.args || ;

      if (context !== "agent") {
        if (channel.state !== "Up") {
          await handleIncomingCall(event, channel, client);
        } else {
          await handleCallEnd(event);
        }
        return;
      }
      
      const callId = callIdArg || channel.variables?.CALL_ID;
      const bridgeId = bridgeIdArg || channel.variables?.BRIDGE_ID;
      
      console.log(`🌉 Agent channel ${channel.id} created for call ${callId}, bridge ${bridgeId}`);
      
      await client.channels.answer({ channelId: channel.id });
      console.log(`✅ Agent channel ${channel.id} answered`);
      
      await client.bridges.addChannel({ bridgeId, channel: channel.id });
      console.log(`✅ Agent channel ${channel.id} added to bridge ${bridgeId}`);
      
      await stopMOH(callId);
      console.log(`🔈 MOH stopped for call ${callId}`);
      

      });

      client.on(“ChannelDtmfReceived”, async (event, channel) => {
      const callInfo = await callDatabaseService.getCallByChannelId(channel.id);
      if (callInfo) {
      await handleDTMF(channel, event.digit, callInfo);
      }
      });

      client.on(“PlaybackFinished”, (ev, playback) => {
      if (playback.channel?.id) {
      console.log(✅ Playback finished for channel ${playback.channel.id});
      }
      });

      client.on(“ChannelDestroyed”, async (event, channel) => {
      const callInfo = await callDatabaseService.getCallByChannelId(channel.id);
      if (callInfo?.metadata?.agentChannelId === channel.id) {
      await callDatabaseService.updateCallStatus(callInfo.callId, {
      metadata: {
      …callInfo.metadata,
      agentJoined: false,
      agentId: null,
      agentChannelId: null,
      agentLeaveTime: new Date().toISOString(),
      },
      });
      }
      });
      }

      What works

      • Agent registers via WebSocket successfully

      • INVITE/200 OK/ACK completes

      • ARI bridge is created and agent channel is added

        What works

        • Agent registers via WebSocket successfully

        • INVITE/200 OK/ACK completes

        • ARI bridge is created and agent channel is added

          Questions for the community

          • Do I need extra config for SRTP/ICE when bridging WebRTC ↔ PSTN/SIP?

          • Should rtp set debug on show incoming/outgoing packets (currently nothing)?

          • Any issue with how I originate & answer the agent channel in ARI?

          • Is there a better pattern for adding a WebRTC endpoint to an ARI bridge?

          Thanks for taking the time to look at this! :folded_hands:

It sounds like you’re running into a classic WebRTC networking issue. The most likely cause is that Asterisk doesn’t know its public IP address, which prevents the ICE process from successfully establishing a media path between the browser and the server. The fact that rtp set debug on shows no packets is the key symptom here.

Your ARI logic for originating the channel and adding it to the bridge is correct. The problem lies in the underlying PJSIP and network configuration.


The Fix: Configure External IP and Local Network

You need to tell Asterisk about your network environment so it can generate correct ICE candidates for the WebRTC client. You do this by adding externip and local_net to your PJSIP transport configuration.

Update your pjsip.conf file like this:

Ini, TOML

[transport-webrtc]
type=transport
protocol=wss
bind=0.0.0.0:8088
; --- Add these two lines ---
externip=YOUR_SERVER_PUBLIC_IP
local_net=YOUR_LOCAL_NETWORK_CIDR ; e.g., 192.168.1.0/24

  • externip: Set this to the public IP address of your Asterisk server.

  • local_net: Set this to your local network’s address range in CIDR notation. This tells Asterisk which addresses are internal and don’t require NAT handling.

After making these changes, restart Asterisk (or at least reload chan_pjsip with pjsip reload) for them to take effect. Also, ensure your firewall allows UDP traffic on your RTP port range (typically 10000-20000, defined in rtp.conf).


Your Specific Questions Answered

  • Do I need extra config for SRTP/ICE when bridging WebRTC ↔ PSTN/SIP? Yes, the externip and local_net settings described above are the crucial extra configuration needed for ICE to work correctly across networks. Your endpoint settings for SRTP (media_encryption=dtls, etc.) are already correct.

  • Should rtp set debug on show incoming/outgoing packets? Yes, absolutely. It should show SRTP packets being sent and received once the call is connected. The fact that it’s empty is the strongest evidence that the ICE negotiation failed and no media path was ever established between the browser and Asterisk.

  • Any issue with how I originate & answer the agent channel in ARI? No, your ARI logic looks great. The sequence of originate, then answer in Stasis, then addChannel to the bridge is a perfectly valid and standard pattern.

  • Is there a better pattern for adding a WebRTC endpoint to an ARI bridge? The pattern you are using is solid. It’s an efficient way to programmatically bring a WebRTC endpoint into an existing call bridge.


If adding the network configuration doesn’t solve the problem, the next step would be to look at the browser’s side of the connection. use your browser’s built-in WebRTC diagnostics (chrome://webrtc-internals) to see why the connection is failing…

This is invalid for PJSIP. Did you use an LLM to construct this answer? If so, it is incorrect. Such usage should also be disclosed.

I apologize, I only used it for formatting and to make me not sound like a jerk. The config and misguided chan_sip was my own.

And, you’re right, I was combining old chansip ideas….

What about this…. PJSIP uses external media and external signaling, and not in the global section - thoughts?

[global]
type=global
local_net=YOUR_LOCAL_NETWORK_CIDR

[transport-webrtc]
type=transport
protocol=wss
bind=0.0.0.0:8088
external_media_address=YOUR_SERVER_PUBLIC_IP
external_signaling_address=YOUR_SERVER_PUBLIC_IP

This defines the local network for all of PJSIP and applies the external public IP address specifically to the WebRTC transport, which is how PJSIP is designed to work.

I’m working on integrating WhatsApp calling into a MERN application and am using Asterisk as the SIP/media server. Here’s the flow so far:

  1. Incoming call via WhatsApp Business API → WhatsApp sends a SIP INVITE to Asterisk.

  2. Asterisk forwards the call to my Node.js service, which answers it and starts an IVR — this part works correctly.

  3. When I try to bridge that caller with an available agent in our React frontend, I use WebRTC (SIP.js) to connect the agent’s browser to Asterisk.

The problem:

  • The WebRTC session establishes, but there’s no audio between the WhatsApp caller and the agent.

  • Registration sometimes fails if I use ws://127.0.0.1:8088/ws. When it does register, calls connect but remain silent.

  • My pjsip.conf has direct_media=no, rtp_symmetric=yes, ice_support=yes, and media_encryption=dtls. I’ve tested with both WS and WSS transports.

    Could you advise:

    • Are there recommended settings for bridging SIP (WhatsApp) to WebRTC endpoints?

    • Should I use a bridge in ARI or let Asterisk handle media automatically?

    • Any best practices for pjsip and rtp.conf to ensure audio flows in a SIP↔WebRTC scenario?

    Any pointers or a working example would be hugely appreciated — I’ve exhausted the usual guides, and even large-language-model suggestions haven’t resolved the audio path issue.

    Thanks in advance

issue resolved

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.