How to get only caller/callee audio and stream to external websocket for STT

When a call is going on, I need to get caller and callee chunks seperately
What i did:

  1. Create a bridge b/w local channels for caller and callee
  2. but when i try to use external media channels into the same bridge, getting mixed audio
  3. Also, tried snoop channel method
    =>create snoop channels for both local channels
    =>create external media channels and brdige them with respective snooop channels
    but still getting mixed audio

That’s how bridges work. You get the mixed audio.

You haven’t stated what arguments you passed. If you only want audio coming from the remote side, you’d specify a spy direction of “in”.

If that doesn’t work then you need to actually show in entirety what is going on, because you are most likely leaving out details that are important.

async function createSnoopChannel(client, targetChan, label, callId) {
try {
// Create snoop channel to listen only to the target channel
const snoop = await client.channels.snoopChannel({
channelId: targetChan.id,
app: APP_NAME,
spy: ‘out’, // need the audio from user
whisper: ‘none’,
appArgs: snoop_${label}:${callId}
});

console.log(`[${label}] Snoop channel created: ${snoop.id} for target channel: ${targetChan.id}`);

if (!agentCalls[callId].snoops) agentCalls[callId].snoops = {};
agentCalls[callId].snoops[label] = { snoop };

// Create UDP socket for this snoop’s external media
const udpSocket = dgram.createSocket('udp4');
const nodePort = getRandomPort();

await new Promise((resolve, reject) => {
  udpSocket.bind(nodePort, '0.0.0.0', async () => {
    console.log(`[${label}] UDP socket bound: 127.0.0.1:${nodePort}`);
    try {
      const externalMedia = await client.channels.externalMedia({
        app: APP_NAME,
        external_host: `127.0.0.1:${nodePort}`,
        format: 'slin16',
        direction: 'in',
        encapsulation: 'rtp'
      });

      const tapBridge = await client.bridges.create({ type: 'mixing' });
      await tapBridge.addChannel({ channel: [snoop.id, externalMedia.id] });

      // RTP packet handler
      udpSocket.on('message', (msg) => {
        if (msg.length > 12) {
          const version = (msg[0] & 0b11000000) >> 6;
          if (version !== 2) return;

          const padding = (msg[0] & 0b00100000) >> 5;
          const csrcCount = msg[0] & 0b00001111;
          const headerLength = 12 + csrcCount * 4;
          let payloadLength = msg.length - headerLength;

          if (padding) {
            const padBytes = msg[msg.length - 1];
            payloadLength -= padBytes;
          }
          if (payloadLength <= 0) return;

          let rtpPayload = msg.slice(headerLength, headerLength + payloadLength);

          // endian swap
          for (let i = 0; i < rtpPayload.length; i += 2) {
            const tmp = rtpPayload[i];
            rtpPayload[i] = rtpPayload[i + 1];
            rtpPayload[i + 1] = tmp;
          }
          // For processing the audio from user
          sendAudioToWs(agentCalls[callId].ws, label, rtpPayload, callId);
        }
      });

      // Store refs
      agentCalls[callId].snoops[label].externalMedia = externalMedia;
      agentCalls[callId].snoops[label].udpSocket = udpSocket;

      udpSocket.on('close', () => console.log(`[UDP] ${label} closed ${nodePort}`));
      udpSocket.on('error', (err) => console.error(`[UDP] ${label} error ${nodePort}: ${err.message}`));

      resolve(externalMedia);
    } catch (err) {
      reject(err);
    }
  });
});

console.log(`[${label}] External media channel created successfully (isolated)`);

} catch (err) {
console.error([${label}] Failed to create snoop channel: ${err.message});
}
}

ari.connect(ARI_URL, ARI_USER, ARI_PASS, async (err, client) => {
if (err) { throw err; }
console.log(‘[ARI] Connected’);

client.on(‘StasisStart’, async (event, channel) => {
const argsString = (event.args && event.args.length > 0) ? event.args[0] : ‘’;
const args = argsString.split(‘:’);
const type = args[0] || ‘unknown’;
const callId = args[1] || null;
const customerNum = args[2] || null;
const agent = args[3] || null;

console.log(`[StasisStart] ${type} channel: ${channel.id}, callId=${callId}, agent=${agent}`);

if (!callId) return;

try {
  await channel.answer();
  console.log(`[${type}] Answered: ${channel.id}`);

  if (!agentCalls[callId]) {
    agentCalls[callId] = {
      cleanedUp: false,
      udp: { in: {}, out: {} },
      extMedia: { in: {}, out: {} },
      recordings: {},
      snoops: {},
      recordingBridges: {},
      sequenceNumber: { agent: 0, customer: 0 },
      mediaPacketCount: { agent: 0, customer: 0 },
      mediaBytesCount: { agent: 0, customer: 0 },
      chunkCounters: { agent: 0, customer: 0 },
      streamSid: `stream-${callId}-${Date.now()}`,
      streamStartTime: Date.now()
    };
  }
  const call = agentCalls[callId];
  call[`${type}Chan`] = channel;

  if (!call.ws) {
    call.ws = new WebSocket(VOICEBOT_URL);
    call.ws.on('open', () => {
      console.log(`[WS] Connected to bot for ${callId}`);
    });
    call.ws.on('close', () => console.log(`[WS] Closed for ${callId}`));
    call.ws.on('error', (err) => console.error(`[WS] Error for ${callId}: ${err.message}`));
    call.ws.on('message', (msg) => {
      //console.log(`[WS:IN] Received bot message for ${callId}: ${msg.toString().substring(0, 100)}...`);
      // }
    });
  }

  if (type === 'agent') {
    const bridge = await client.bridges.create({ type: 'mixing' });
    call.bridge = bridge;
    await bridge.addChannel({ channel: [channel.id]});
    console.log(`[Bridge] Agent channels added to bridge`);

    if (customerNum) {
      try {
        const custChan = await client.channels.originate({
          endpoint: `SIP/genesys-trunk/${customerNum}`,
          app: APP_NAME,
          appArgs: `customer:${callId}:${customerNum}:${agent}`,
          callerId: agent,
          timeout: 30,
          earlyMedia: true,
          statusEvents: true
        });
        call.custChan = custChan;
        console.log(`[Originate] Customer call started: ${customerNum}`);
      } catch (err) {
        console.error(`[Originate] Failed to start customer: ${err.message}`);
      }
    }
  } else if (type === 'customer' && call.bridge) {
    call.custChan = channel;
    console.log(`[Customer] PSTN channel created: ${channel.id}`);
    try {
      await call.bridge.addChannel({ channel: [call.custChan.id] });
      console.log(`[Customer] Customer channels added to bridge`);

        // Wait a moment for channels to be fully established
        await new Promise(resolve => setTimeout(resolve, 1000));
        
        // Create Snoop Channels to listen to individual channels
        await createSnoopChannel(client, call.agentChan, 'agent', callId);
        await createSnoopChannel(client, call.custChan, 'customer', callId);
        console.log(`[Snoop] Snoop channels created for both agent and customer`);
       
    } catch (err) {
      console.error(`[Customer] Failed to add customer to bridge: ${err.message}`);
      await channel.hangup();
    }
  }

} catch (err) {
  console.error(`[${type}] Error in StasisStart:`, err);
  await cleanupCall(callId);
}

});

client.on(‘ChannelHangupRequest’, (event, channel) => {
for (const callId in agentCalls) {
const call = agentCalls[callId];
if (call && ((call.agentChan && call.agentChan.id === channel.id) || (call.custChan && call.custChan.id === channel.id))) {
cleanupCall(callId);
break;
}
}
});

client.on(‘StasisEnd’, (event, channel) => {
for (const callId in agentCalls) {
const call = agentCalls[callId];
if (call && ((call.agentChan && call.agentChan.id === channel.id) || (call.custChan && call.custChan.id === channel.id))) {
cleanupCall(callId);
break;
}
}
});

client.start(APP_NAME);
});

in = audio coming into Asterisk from the user

out = audio going out of Asterisk to the user

Oh got it, will try

const tapBridge = await client.bridges.create({ type: ‘mixing’ });

await tapBridge.addChannel({ channel: [snoop.id, externalMedia.id] });

I’m adding the external media channel and snoop channel to a bridge,
1. Is it required and
2.Is it fine?(Like, will I get only user1 audio not mixed one??)

If you want to send media from a snoop channel to external media, then a bridge is how you do it. The external media will get what the snoop channel gets from snooping.

An additional note, I’m seeing arguments to ARI routes that don’t exist. If you’re using AI to write this, be aware it is lying to you.

Could you please point out some of those?

I have changed the direction and removed wrong params, now it’s working fine
Thank you

The ones that stood out were these. These don’t exist.

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