Android VoIP应用UDP包重排需求:无RTP/SIP解决互联网抖动音质问题
嘿,我正好折腾过纯UDP的VoIP场景,完全懂你不想碰RTP省编码的想法!针对互联网环境的抖动和包乱序问题,给你一套落地的接收端重排+缓冲方案,不用依赖任何现成协议:
核心前提:给UDP包加自定义头部
没有RTP的序列号和时间戳,你得自己给每个UDP包加个极简头部——这是实现重排的基础,花不了几分钟:
- 至少包含两个字段:32位递增序列号(从通话开始每次+1)、32位发送时间戳(比如用发送端的系统毫秒数,或者通话启动后的相对毫秒数)
- 发送端示例(Kotlin):
// 给音频数据套上自定义头部 fun wrapAudioPacket(audioFrame: ByteArray, seq: Int, sendTimestamp: Long): ByteArray { val buffer = ByteBuffer.allocate(8 + audioFrame.size) buffer.putInt(seq) // 序列号,每次发送+1 buffer.putInt(sendTimestamp.toInt()) // 相对时间戳,避免溢出用int足够支撑几小时通话 buffer.put(audioFrame) return buffer.array() }
接收端重排+缓冲实现步骤
1. 选择合适的缓冲结构
用TreeMap<Int, ByteArray>(按序列号自动排序)来存未处理的包,同时维护一个变量lastProcessedSeq记录已经输出的最大序列号——这样能快速找到下一个该输出的包。
2. 抖动缓冲窗口控制
设置一个可配置的抖动窗口(比如默认200ms,可动态调整),核心逻辑是:
不要刚收到包就输出,等一等,让乱序的包有时间赶上来;但也不能等太久,不然延迟会高到影响通话体验。
3. 核心处理逻辑
分两个线程配合(或者用Handler定时触发):
- 接收线程:持续收包,解析头部(取出序列号和时间戳),把完整的音频数据放到TreeMap里
- 输出线程/定时任务:每10ms左右检查一次缓冲,执行以下操作:
- 从TreeMap里找
lastProcessedSeq + 1对应的包,如果存在,就依次往后遍历连续的序列号,把这些包的音频数据拼接起来,输出到扬声器 - 更新
lastProcessedSeq到最后一个输出的序列号,把已经输出的包从TreeMap里移除 - 检查缓冲里最早的包的时间戳:如果当前时间 - 包的发送时间戳 > 抖动窗口,不管有没有连续的包,都把能输出的连续部分先输出,丢弃剩下的过期包(避免缓冲无限膨胀)
- 从TreeMap里找
4. 关键边界处理
- 序列号回绕:32位序列号到最大值会回到0,比较的时候要加判断:如果新序列号比
lastProcessedSeq小很多(比如差超过2^31),就认为是回绕了,正常处理 - 丢包处理:如果某个序列号的包超过抖动窗口还没收到,直接跳过这个位置,插入静音帧或者用前一帧的音频补全,避免卡住整个播放流程
优化建议
- 动态调整抖动窗口:统计最近100个包的延迟抖动(比如计算延迟的标准差),自动调整窗口大小——网络差的时候调到300ms,稳定的时候降到150ms,平衡音质和延迟
- 换成环形缓冲区:如果序列号是严格递增的,用固定大小的环形数组代替TreeMap,效率更高,适合高并发场景(用
seq % bufferSize定位存储位置) - 音频帧对齐:确保每个UDP包的音频数据是完整的帧(比如PCM每帧固定10ms的字节数),这样输出的时候不会出现音频断裂的情况
内容的提问来源于stack exchange,提问作者poorcoder
相关产品推荐
相关产品推荐

