You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Asterisk ARI外部媒体通道回传音频无声音问题排查

问题:Asterisk通过ExternalMedia回传RTP音频无法被接收方听到

场景说明

  • 发起呼叫并触发Stasis应用
  • 创建ExternalMediaChannel,将音频流发送至监听0.0.0.0:7777的自建RTP服务器
  • 创建mixing类型的Bridge,将呼叫通道与ExternalMediaChannel加入其中,目标是让发起呼叫的通道能听到回传的音频

当前状态

RTP服务器已确认接收并能正常解析有效音频(保存为文件可播放),且已回传RTP包至Asterisk指定的地址,但发起呼叫的通道听不到声音。

ARI ExternalMediaChannel信息

{"UNICASTRTP_LOCAL_PORT":"15672","UNICASTRTP_LOCAL_ADDRESS":"172.18.33.220"}

Asterisk日志

Got  RTP packet from    10.114.0.234:55874 (type 111, seq 031742, ts 3934092822, len 000069)

Sent RTP packet to      172.18.33.220:7777 (type 118, seq 029097, ts 1090576, len 000640)

Got  RTP packet from    10.114.0.234:55874 (type 111, seq 031743, ts 3934093782, len 000071)

Sent RTP packet to      172.18.33.220:7777 (type 118, seq 029098, ts 1090896, len 000640)

RTP服务器日志

Sending To: 172.18.33.220:15672

Received From: 172.18.33.220:15672

Sending To: 172.18.33.220:15672

Received From: 172.18.33.220:15672

RTP服务器代码(C#)

public class UDPListener
{
    private const int ListenPort = 7777;

    public static void StartListener()
    {
        UdpClient server = new UdpClient(ListenPort);
        IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, ListenPort);

        Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

        try
        {
            Console.WriteLine("Listening to: " + ListenPort);

            while (true)
            {
                byte[] bytes = server.Receive(ref groupEP);
                // PRINTS THE BYTE CODES
                // Console.WriteLine($" {Encoding.ASCII.GetString(bytes, 0, bytes.Length)}");

                RtpPacket rcvpacket;
                bool parsedRCV = RtpPacket.TryParse(bytes, out rcvpacket);
                if(parsedRCV){
                    Console.WriteLine($"Received From: {groupEP.Address}:{groupEP.Port}");
                }                   

                RtpPacket packet;
                bool parsed = RtpPacket.TryParse(bytes, out packet);
                if(parsed){
                    Console.WriteLine($"Sending To: {groupEP.Address}:{groupEP.Port}");
                    client.SendTo(packet.PayloadSegment, groupEP);
                }
            }
        }            
    }
}

RtpPacket定义(C#)

struct RtpPacket
{
    public const int RtpHeaderSize = 12;
    public const int RtpProtocolVersion = 2;

    public int ProtocolVersion { get; private set; }
    public bool PaddingFlag { get; private set; }
    public bool ExtensionFlag { get; private set; }
    public int CsrcCount { get; private set; }
    public bool MarkerBit { get; private set; }
    public int PayloadType { get; private set; }
    public ushort SeqNumber { get; private set; }
    public uint Timestamp { get; private set; }
    public uint SyncSourceId { get; private set; }
    public int ExtensionHeaderId { get; private set; }

    public ArraySegment<byte> PayloadSegment { get; set; }

    internal RtpPacket(ushort seqNumber, ArraySegment<byte> payloadSegment)
    {
        ProtocolVersion = 1;
        PaddingFlag = false;
        ExtensionFlag = false;
        CsrcCount = 0;
        MarkerBit = false;
        PayloadType = 0;
        SeqNumber = 0;
        Timestamp = 0;
        SyncSourceId = 0;
        ExtensionHeaderId = 0;
        SeqNumber = seqNumber;
        PayloadSegment = payloadSegment;
    }

    public static bool TryParse(ArraySegment<byte> byteSegment, out RtpPacket rtpPacket)
    {
        rtpPacket = new RtpPacket();

        Debug.Assert(byteSegment.Array != null, "byteSegment.Array != null");

        if (byteSegment.Count < RtpHeaderSize)
            return false;

        int offset = byteSegment.Offset;
        rtpPacket.ProtocolVersion = byteSegment.Array[offset] >> 6;

        if (rtpPacket.ProtocolVersion != RtpProtocolVersion)
            return false;

        rtpPacket.PaddingFlag = (byteSegment.Array[offset] >> 5 & 1) != 0;
        rtpPacket.ExtensionFlag = (byteSegment.Array[offset] >> 4 & 1) != 0;
        rtpPacket.CsrcCount = byteSegment.Array[offset++] & 0xF;

        rtpPacket.MarkerBit = byteSegment.Array[offset] >> 7 != 0;
        rtpPacket.PayloadType = byteSegment.Array[offset++] & 0x7F;

        rtpPacket.SeqNumber = (ushort)BigEndianConverter.ReadUInt16(byteSegment.Array, offset);
        offset += 2;

        rtpPacket.Timestamp = BigEndianConverter.ReadUInt32(byteSegment.Array, offset);
        offset += 4;

        rtpPacket.SyncSourceId = BigEndianConverter.ReadUInt32(byteSegment.Array, offset);
        offset += 4 + 4 * rtpPacket.CsrcCount;

        if (rtpPacket.ExtensionFlag)
        {
            rtpPacket.ExtensionHeaderId = BigEndianConverter.ReadUInt16(byteSegment.Array, offset);
            offset += 2;

            int extensionHeaderLength = BigEndianConverter.ReadUInt16(byteSegment.Array, offset) * 4;
            offset += 2 + extensionHeaderLength;
        }

        int payloadSize = byteSegment.Offset + byteSegment.Count - offset;

        if (rtpPacket.PaddingFlag)
        {
            int paddingBytes = byteSegment.Array[byteSegment.Offset + byteSegment.Count - 1];
            payloadSize -= paddingBytes;
        }

        rtpPacket.PayloadSegment = new ArraySegment<byte>(byteSegment.Array, offset, payloadSize);
        return true;
    }
}

public static class BigEndianConverter
{
    public static uint ReadUInt32(byte[] buffer, int offset)
    {
        return (uint)(buffer[offset] << 24 |
                       buffer[offset + 1] << 16 |
                       buffer[offset + 2] << 8 |
                       buffer[offset + 3]);
    }

    public static int ReadUInt24(byte[] buffer, int offset)
    {
        return buffer[offset] << 16 |
               buffer[offset + 1] << 8 |
               buffer[offset + 2];
    }

    public static int ReadUInt16(byte[] buffer, int offset)
    {
        return (buffer[offset] << 8) | buffer[offset + 1];
    }
}

问题分析与解决方案

核心问题:回传的仅为RTP负载,缺少RTP头部

从代码中可以看到,client.SendTo(packet.PayloadSegment, groupEP)仅发送了RTP包的负载部分,而Asterisk需要完整的RTP包(包含头部)才能正确解析音频。

解决步骤

  1. 构建完整的RTP包:回传时需要重新组装包含正确头部的RTP数据包,而不是只发送负载。
    • 需要保留或正确设置以下头部字段:
      • 协议版本(必须为2)
      • 负载类型(PayloadType,要和Asterisk发送的一致,即日志中的118)
      • 序列号(SeqNumber,需按顺序递增)
      • 时间戳(Timestamp,需与音频采样率同步递增)
      • SSRC(可以复用原包的SSRC,或使用新的唯一值)
  2. 修正发送逻辑:在TryParse成功后,基于原包的头部信息(或构造合法头部),将头部与负载拼接成完整的RTP字节数组再发送。

代码修正示例

在UDPListener的循环中,替换发送逻辑:

if(parsed){
    Console.WriteLine($"Sending To: {groupEP.Address}:{groupEP.Port}");
    // 构建完整的RTP包
    byte[] fullRtpPacket = BuildFullRtpPacket(packet);
    client.SendTo(fullRtpPacket, groupEP);
}

添加BuildFullRtpPacket方法:

private static byte[] BuildFullRtpPacket(RtpPacket packet)
{
    int packetSize = RtpPacket.RtpHeaderSize + packet.PayloadSegment.Count;
    byte[] buffer = new byte[packetSize];
    int offset = 0;

    // 版本(2) + 填充(0) + 扩展(0) + CSRC计数(0)
    buffer[offset++] = (byte)((RtpPacket.RtpProtocolVersion << 6) | (0 << 5) | (0 << 4) | 0);
    // 标记位(0) + 负载类型
    buffer[offset++] = (byte)((0 << 7) | packet.PayloadType);
    // 序列号(大端)
    byte[] seqBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)packet.SeqNumber));
    Buffer.BlockCopy(seqBytes, 0, buffer, offset, 2);
    offset += 2;
    // 时间戳(大端)
    byte[] tsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)packet.Timestamp));
    Buffer.BlockCopy(tsBytes, 0, buffer, offset, 4);
    offset += 4;
    // SSRC(大端)
    byte[] ssrcBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((int)packet.SyncSourceId));
    Buffer.BlockCopy(ssrcBytes, 0, buffer, offset, 4);
    offset += 4;
    // 复制负载
    Buffer.BlockCopy(packet.PayloadSegment.Array, packet.PayloadSegment.Offset, buffer, offset, packet.PayloadSegment.Count);

    return buffer;
}

其他排查点

  • 确认回传的RTP包的PayloadType与Asterisk发送的一致(日志中为118)
  • 检查序列号是否连续递增,时间戳是否按采样率正确递增(比如PCMU/PCMA采样率为8000,每包时间戳增加160对应20ms的音频)
  • 确认Asterisk的RTP配置允许接收该PayloadType的音频
  • 检查网络是否存在丢包或延迟,可通过Wireshark抓包验证回传的RTP包是否完整到达Asterisk

内容的提问来源于stack exchange,提问作者Flamur Dervishi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 23:20:33