使用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包(包含头部)才能正确解析音频。
解决步骤
- 构建完整的RTP包:回传时需要重新组装包含正确头部的RTP数据包,而不是只发送负载。
- 需要保留或正确设置以下头部字段:
- 协议版本(必须为2)
- 负载类型(PayloadType,要和Asterisk发送的一致,即日志中的118)
- 序列号(SeqNumber,需按顺序递增)
- 时间戳(Timestamp,需与音频采样率同步递增)
- SSRC(可以复用原包的SSRC,或使用新的唯一值)
- 需要保留或正确设置以下头部字段:
- 修正发送逻辑:在
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
相关产品推荐
相关产品推荐

