librtp 实现详解:仓颉语言中的 RTP和RTCP 协议库开发实践

librtp 实现详解:仓颉语言中的 RTP/RTCP 协议库开发实践

前言

RTP(Real-time Transport Protocol,实时传输协议)和 RTCP(RTP Control Protocol,RTP 控制协议)是音视频流媒体传输的核心协议,广泛应用于视频会议、直播、VoIP 等实时通信场景。在仓颉语言生态中,librtp 库提供了完整的 RTP/RTCP 协议处理能力,支持包的创建、读取、序列化、解析等功能,为开发者提供了高效、可靠的流媒体传输解决方案。

本文将从库的设计思路、核心实现、技术挑战、性能优化等多个维度,深入解析 librtp 库的开发过程,为仓颉语言开发者提供库开发的实践参考。

一、库概述

1.1 项目背景

在实时音视频通信、流媒体传输、网络监控等场景中,RTP/RTCP 协议是确保数据正确传输和实时反馈的关键技术。RTP 负责传输音视频数据,RTCP 负责传输控制信息,两者配合工作,共同保障实时通信的质量。

librtp 库旨在为仓颉语言提供一套完整、高效、易用的 RTP/RTCP 协议处理解决方案,支持 RTP 包的创建、读取和序列化,RTCP 包的创建和解析,NTP 时间戳和 RTP 时间戳的转换,以及 RTP 抖动缓冲管理等功能。

1.2 核心特性

librtp 库具有以下核心特性:

  • RTP 包处理:支持 RTP 包的创建、读取和序列化,包括头部操作、扩展头支持、填充处理等
  • RTCP 包处理:支持多种 RTCP 包类型的创建和解析,包括 SR、RR、SDES、BYE、APP、RTPFB 等
  • 时间戳处理:支持 NTP 时间戳和 RTP 时间戳的转换,包括 64 位和 32 位 NTP 时间戳的处理
  • 抖动缓冲:提供完整的 RTP 抖动缓冲管理功能,支持包的入队、出队、处理、信息获取等
  • 类型安全:充分利用仓颉语言的类型系统,确保类型安全
  • 自动内存管理:无需手动管理内存,减少内存泄漏风险
  • 易于使用:提供简洁的 API 接口,支持网络字节序自动转换

1.3 技术栈

  • 编程语言:仓颉(Cangjie)
  • 构建工具:CJPM(Cangjie Package Manager)
  • 测试框架:仓颉标准测试框架
  • 文档工具:Markdown

二、核心功能实现

2.1 RTP 协议基础

RTP 协议是一种用于实时数据传输的传输层协议,通常运行在 UDP 之上。RTP 包由固定头部(12 字节)和可变负载组成,头部包含版本号、填充标志、扩展标志、CSRC 计数、标记位、负载类型、序列号、时间戳、SSRC 等字段。

RTP 包头部结构

RTP 包头部固定为 12 字节,结构如下:

复制代码
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier              |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

其中:

  • V(Version):版本号,当前为 2
  • P(Padding):填充标志,表示包末尾是否有填充字节
  • X(Extension):扩展标志,表示头部后是否有扩展头
  • CC(CSRC Count):CSRC 计数,表示 CSRC 标识符的数量
  • M(Marker):标记位,用于标记重要事件(如视频帧边界)
  • PT(Payload Type):负载类型,标识负载的编码格式
  • Sequence Number:序列号,用于检测丢包和乱序
  • Timestamp:时间戳,用于同步和播放
  • SSRC:同步源标识符,唯一标识数据源
RTP 包创建和序列化

librtp 库提供了 rtpPktNew() 函数创建新的 RTP 包,rtpPktFinalizeHeader() 函数序列化 RTP 包头部:

cangjie 复制代码
// 创建 RTP 包
let pkt = rtpPktNew()
pkt.header.seqnum = 12345u16
pkt.header.timestamp = 67890u32
pkt.header.ssrc = 0x12345678u32

// 设置负载数据
pkt.rawData = [0x01u8, 0x02u8, 0x03u8, 0x04u8]
pkt.payloadOff = 0i64
pkt.payloadLen = 4i64

// 序列化 RTP 包
let dataOpt = rtpPktFinalizeHeader(pkt)
match (dataOpt) {
    case Some(data) => {
        // 使用序列化后的数据(网络字节序)
    }
    case None => {
        // 序列化失败
    }
}

关键实现细节

  1. 字节序转换:RTP 包在网络传输时使用网络字节序(大端序),序列化时需要将主机字节序转换为网络字节序
  2. 头部字段编码:将多个字段编码到一个 16 位的 flags 字段中,使用位操作进行读写
  3. 负载数据管理 :使用 rawData 数组存储原始数据,通过 payloadOffpayloadLen 标识负载位置和长度
RTP 包读取和解析

librtp 库提供了 rtpPktRead() 函数从字节数组读取 RTP 包:

cangjie 复制代码
// 构造 RTP 包数据(网络字节序)
let data: Array<UInt8> = [
    0x60u8, 0x80u8,  // flags: 版本2, 负载类型=96
    0x39u8, 0x30u8,  // 序列号 12345 (网络字节序)
    0x32u8, 0x09u8, 0x01u8, 0x00u8,  // 时间戳 67890 (网络字节序)
    0x78u8, 0x56u8, 0x34u8, 0x12u8,  // SSRC 0x12345678 (网络字节序)
    0x01u8, 0x02u8, 0x03u8, 0x04u8   // 负载数据
]

// 读取 RTP 包
let pktOpt = rtpPktRead(data)
match (pktOpt) {
    case Some(pkt) => {
        // 使用解析后的 RTP 包(主机字节序)
        println("序列号: " + pkt.header.seqnum.toString())
        println("时间戳: " + pkt.header.timestamp.toString())
        println("SSRC: 0x" + pkt.header.ssrc.toString(16))
    }
    case None => {
        // 读取失败
    }
}

关键实现细节

  1. 字节序转换:读取时需要将网络字节序转换为主机字节序
  2. 版本检查:验证 RTP 协议版本是否为 2
  3. 扩展头处理:如果扩展标志为 1,则解析扩展头
  4. 填充处理:如果填充标志为 1,则解析填充字节
  5. CSRC 处理:根据 CSRC 计数跳过 CSRC 标识符

2.2 RTCP 协议基础

RTCP 协议是 RTP 的配套协议,用于传输控制信息,包括发送者报告(SR)、接收者报告(RR)、源描述(SDES)、离开(BYE)、应用定义(APP)、RTP 反馈(RTPFB)等包类型。

RTCP 包头部结构

RTCP 包头部固定为 4 字节,结构如下:

复制代码
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|    RC   |   PT=SR=200   |             length             |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

其中:

  • V(Version):版本号,当前为 2
  • P(Padding):填充标志,表示包末尾是否有填充字节
  • RC(Reception Report Count):接收报告计数,表示报告块的数量
  • PT(Packet Type):包类型,如 SR(200)、RR(201)、SDES(202)等
  • Length:长度,以 32 位字为单位,不包括头部
RTCP 发送者报告(SR)

RTCP 发送者报告包含发送者的统计信息和接收报告块:

cangjie 复制代码
// 创建 RTCP 发送者报告
let sr = RtcpPktSenderReport()
sr.ssrc = 0x12345678u32
sr.ntpTimestamp.seconds = 1234567890u32
sr.ntpTimestamp.fraction = 0x40000000u32
sr.rtpTimestamp = 67890u32
sr.senderPacketCount = 1000u32
sr.senderByteCount = 50000u32

// 添加接收报告块
let rb = RtcpPktReportBlock()
rb.ssrc = 0x87654321u32
rb.fraction = 0u8
rb.lost = 0i32
rb.extHighestSeqnum = 12345u32
rb.jitter = 0u32
sr.reports.add(rb)
sr.reportCount = 1u32

// 序列化 RTCP 包
let dataOpt = rtcpPktWriteSenderReport(sr)
RTCP 接收者报告(RR)

RTCP 接收者报告包含接收者的统计信息:

cangjie 复制代码
// 创建 RTCP 接收者报告
let rr = RtcpPktReceiverReport()
rr.ssrc = 0x12345678u32

// 添加接收报告块
let rb = RtcpPktReportBlock()
rb.ssrc = 0x87654321u32
rb.fraction = 5u8  // 5% 丢包率
rb.lost = 10i32    // 丢失 10 个包
rb.extHighestSeqnum = 12345u32
rb.jitter = 100u32
rr.reports.add(rb)
rr.reportCount = 1u32

// 序列化 RTCP 包
let dataOpt = rtcpPktWriteReceiverReport(rr)
RTCP 包解析

librtp 库提供了 rtcpPktRead() 函数解析 RTCP 包,使用回调函数处理不同类型的包:

cangjie 复制代码
// 创建回调
let cbs = RtcpPktReadCbs()
cbs.senderReport = Some(func(sr: RtcpPktSenderReport): Unit {
    println("收到 SR 包,SSRC: 0x" + sr.ssrc.toString(16))
    println("发送包数: " + sr.senderPacketCount.toString())
    println("发送字节数: " + sr.senderByteCount.toString())
})
cbs.receiverReport = Some(func(rr: RtcpPktReceiverReport): Unit {
    println("收到 RR 包,SSRC: 0x" + rr.ssrc.toString(16))
    println("报告块数: " + rr.reportCount.toString())
})

// 解析 RTCP 包
let success = rtcpPktRead(data, cbs)

关键实现细节

  1. 复合包处理:多个 RTCP 包可以组合成一个复合包,通过长度字段分隔
  2. 版本检查:验证 RTCP 协议版本是否为 2
  3. 回调机制:使用回调函数处理不同类型的包,提供灵活的处理方式
  4. 字节序转换:读取和写入时需要正确进行字节序转换

2.3 NTP 时间戳处理

NTP(Network Time Protocol)时间戳用于同步网络时间,在 RTCP 包中使用。NTP 时间戳由秒数(seconds)和分数(fraction)组成,分数部分表示秒的小数部分。

NTP 64 位时间戳

NTP 64 位时间戳的精度为 2^-32 秒(约 232 皮秒):

cangjie 复制代码
// 创建 NTP 64 位时间戳
let t1 = NtpTimestamp64(1u32, 0x40000000u32)
let t2 = NtpTimestamp64(1u32, 0x20000000u32)

// 计算时间差(微秒)
let diff = ntpTimestamp64DiffUs(t1, t2)
println("时间差: " + diff.toString() + " 微秒")

// 转换为微秒
let us = ntpTimestamp64ToUs(t1)
println("微秒数: " + us.toString())

// 从微秒创建
let t3 = NtpTimestamp64()
ntpTimestamp64FromUs(t3, 1000000u64)
NTP 32 位时间戳

NTP 32 位时间戳的精度为 2^-16 秒(约 15.26 微秒),用于 RTCP 报告块:

cangjie 复制代码
// 创建 NTP 32 位时间戳
let t1 = NtpTimestamp32(1u16, 0x4000u16)
let t2 = NtpTimestamp32(1u16, 0x2000u16)

// 计算时间差(微秒)
let diff = ntpTimestamp32DiffUs(t1, t2)

// 转换为微秒
let us = ntpTimestamp32ToUs(t1)

// 从微秒创建
let t3 = NtpTimestamp32()
ntpTimestamp32FromUs(t3, 1000000u64)
时间戳转换

librtp 库提供了 NTP 时间戳与 timespec 的转换函数:

cangjie 复制代码
// NTP 64 位时间戳转换为 timespec
let t = NtpTimestamp64(1234567890u32, 0x40000000u32)
let ts = Timespec()
ntpTimestamp64ToTimespec(t, ts)
println("秒: " + ts.tvSec.toString() + ", 纳秒: " + ts.tvNsec.toString())

// 从 timespec 创建 NTP 64 位时间戳
let t2 = NtpTimestamp64()
ntpTimestamp64FromTimespec(t2, ts)

2.4 RTP 时间戳处理

RTP 时间戳用于标识负载数据的采样时间,时钟频率取决于负载类型(如音频 8000 Hz,视频 90000 Hz)。

RTP 时间戳转换

librtp 库提供了 RTP 时间戳与微秒的转换函数:

cangjie 复制代码
// RTP 时间戳转换为微秒(时钟频率 90000 Hz)
let rtpTimestamp = 90000u64
let clkRate = 90000u32
let us = rtpTimestampToUs(rtpTimestamp, clkRate)
println("微秒数: " + us.toString())  // 输出: 1000000

// 微秒转换为 RTP 时间戳
let rtpTs = rtpTimestampFromUs(1000000u64, clkRate)
println("RTP 时间戳: " + rtpTs.toString())  // 输出: 90000

转换公式

  • 微秒 = RTP 时间戳 × 1000000 / 时钟频率
  • RTP 时间戳 = 微秒 × 时钟频率 / 1000000

2.5 RTP 抖动缓冲

RTP 抖动缓冲用于处理网络抖动和乱序包,确保数据按顺序、按时播放。

抖动缓冲配置
cangjie 复制代码
// 创建抖动缓冲配置
let cfg = RtpJitterCfg(90000u32, 20000u32)  // 时钟频率 90000 Hz,延迟 20 ms

// 创建回调
let cbs = RtpJitterCbs()
cbs.processPkt = Some(func(jitter: RtpJitter, pkt: RtpPkt, gap: UInt32): Unit {
    println("处理包,序列号: " + pkt.header.seqnum.toString() + ", gap: " + gap.toString())
})

// 创建抖动缓冲
let jitter = RtpJitter(cfg, cbs, None)
包入队和处理
cangjie 复制代码
// 创建 RTP 包
let pkt = rtpPktNew()
pkt.header.seqnum = 1u16
pkt.header.timestamp = 90000u32
pkt.inTimestamp = 1000000u64  // 接收时间戳(微秒)
pkt.rtpTimestamp = 90000u64

// 入队
jitter.enqueue(pkt)

// 处理
jitter.process(1020000u64)  // 当前时间戳(微秒)

// 获取信息
let (packetCount, byteCount, size) = jitter.getInfo()
println("包数量: " + packetCount.toString())
println("字节数: " + byteCount.toString())
println("缓冲大小: " + size.toString())

关键实现细节

  1. 偏移量计算:使用滑动窗口算法计算接收时间戳和 RTP 时间戳之间的偏移量,支持时钟漂移检测和补偿
  2. 抖动计算:使用指数移动平均算法计算网络抖动,用于调整缓冲延迟
  3. 包排序:包按序列号排序存储,支持乱序包的插入
  4. 出队策略 :包在满足以下条件之一时出队:
    • 序列号是下一个期望的序列号
    • 当前时间戳 >= 输出时间戳 + 延迟

三、技术挑战与解决方案

3.1 字节序转换

挑战:RTP/RTCP 包在网络传输时使用网络字节序(大端序),而主机可能使用小端序,需要进行正确的字节序转换。

解决方案:实现字节序转换函数,在读取和写入时自动进行转换:

cangjie 复制代码
// 读取 16 位无符号整数(网络字节序 -> 主机字节序)
private func readUInt16(data: Array<UInt8>, pos: Int64): Option<UInt16> {
    if (pos + 1i64 >= data.size) {
        return None
    }
    let b0 = data[Int(pos)]
    let b1 = data[Int(pos + 1i64)]
    let value = (UInt16(b0) << 8u16) | UInt16(b1)
    // 网络字节序是大端序,需要转换为主机字节序
    let hostValue = ((value & 0x00FFu16) << 8u16) | ((value & 0xFF00u16) >> 8u16)
    return Some(hostValue)
}

// 写入 16 位无符号整数(主机字节序 -> 网络字节序)
private func writeUInt16(data: ArrayList<UInt8>, value: UInt16): Unit {
    // 主机字节序转换为网络字节序(大端序)
    let networkValue = ((value & 0x00FFu16) << 8u16) | ((value & 0xFF00u16) >> 8u16)
    data.add(UInt8((networkValue >> 8u16) & 0xFFu16))
    data.add(UInt8(networkValue & 0xFFu16))
}

注意:在实际实现中,需要根据主机字节序进行条件编译或运行时检测,确保转换的正确性。

3.2 头部字段编码

挑战:RTP 包头部将多个字段编码到一个 16 位的 flags 字段中,需要使用位操作进行读写。

解决方案:实现头部字段的读取和设置函数:

cangjie 复制代码
// 读取 RTP 包头部字段
public func rtpPktHeaderFlagsGet(flags: UInt16, field: String): UInt32 {
    match (field) {
        case "VERSION" => return UInt32((flags >> 14u16) & 0x03u16)
        case "PADDING" => return UInt32((flags >> 13u16) & 0x01u16)
        case "EXTENSION" => return UInt32((flags >> 12u16) & 0x01u16)
        case "CSRC" => return UInt32((flags >> 8u16) & 0x0Fu16)
        case "MARKER" => return UInt32((flags >> 7u16) & 0x01u16)
        case "PAYLOAD" => return UInt32(flags & 0x7Fu16)
        case _ => return 0u32
    }
}

// 设置 RTP 包头部字段
public func rtpPktHeaderFlagsSet(flags: UInt16, field: String, value: UInt32): UInt16 {
    var result = flags
    match (field) {
        case "VERSION" => {
            result = (result & 0x3FFFu16) | (UInt16(value & 0x03u32) << 14u16)
        }
        case "PADDING" => {
            result = (result & 0xDFFFu16) | (UInt16(value & 0x01u32) << 13u16)
        }
        case "EXTENSION" => {
            result = (result & 0xEFFFu16) | (UInt16(value & 0x01u32) << 12u16)
        }
        case "CSRC" => {
            result = (result & 0xF0FFu16) | (UInt16(value & 0x0Fu32) << 8u16)
        }
        case "MARKER" => {
            result = (result & 0xFF7Fu16) | (UInt16(value & 0x01u32) << 7u16)
        }
        case "PAYLOAD" => {
            result = (result & 0xFF80u16) | UInt16(value & 0x7Fu32)
        }
        case _ => ()
    }
    return result
}

3.3 扩展头和填充处理

挑战:RTP 包可能包含扩展头和填充,需要正确解析和处理。

解决方案:实现扩展头和填充的读取函数:

cangjie 复制代码
// 读取 RTP 包的扩展头部分
private func rtpPktReadExtension(data: Array<UInt8>, pos: Int64, pkt: RtpPkt): Option<Int64> {
    if (data.size - pos < 4i64) {
        return None
    }
    pkt.extHeaderOff = pos
    let idOpt = readUInt16(data, pos)
    if (idOpt.isNone()) {
        return None
    }
    let id = match (idOpt) {
        case Some(i) => i
        case None => return None
    }
    pkt.extHeaderId = id
    let lenOpt = readUInt16(data, pos + 2i64)
    if (lenOpt.isNone()) {
        return None
    }
    let len = match (lenOpt) {
        case Some(l) => l
        case None => return None
    }
    let extLen = Int64(len) * 4i64 + 4i64
    pkt.extHeaderLen = extLen
    if (data.size - pos < extLen) {
        return None
    }
    return Some(pos + extLen)
}

// 读取 RTP 包的填充部分
private func rtpPktReadPadding(data: Array<UInt8>, pkt: RtpPkt): Option<Unit> {
    if (pkt.payloadLen < 1i64) {
        return None
    }
    let paddingOpt = readUInt8(data, data.size - 1i64)
    if (paddingOpt.isNone()) {
        return None
    }
    let padding = match (paddingOpt) {
        case Some(p) => p
        case None => return None
    }
    let paddingLen = Int64(padding)
    if (pkt.payloadLen < paddingLen) {
        return None
    }
    pkt.payloadLen = pkt.payloadLen - paddingLen
    pkt.paddingOff = pkt.payloadOff + pkt.payloadLen
    pkt.paddingLen = paddingLen
    return Some(())
}

3.4 抖动缓冲算法

挑战:网络抖动和时钟漂移会导致包的到达时间不稳定,需要智能的缓冲算法来平滑播放。

解决方案:实现滑动窗口算法和指数移动平均算法:

cangjie 复制代码
// 计算偏移量
private func computeSkew(rxTimestamp: UInt64, rtpTimestamp: UInt64): UInt64 {
    let clkRate = this.cfg.clkRate
    var deltaSend = rtpTimestamp - this.firstRtpTimestamp
    var deltaRecv: UInt64 = 0u64
    var skew: Int64 = 0i64
    var outTimestamp: UInt64 = 0u64
    
    // 计算发送端增量
    if (deltaSend > rtpTimestamp) {
        // 发送端可能重启了
        deltaSend = 0u64 - rtpTimestampToUs(rtpTimestamp - deltaSend, clkRate)
        this.resetSkew(rxTimestamp, rtpTimestamp)
        deltaSend = 0u64
    } else {
        deltaSend = rtpTimestampToUs(deltaSend, clkRate)
    }
    deltaRecv = rxTimestamp - this.firstRxTimestamp
    
    // 当前偏移量
    skew = Int64(deltaRecv) - Int64(deltaSend)
    
    // 检查大间隙
    if ((skew - this.skewAvg < 0i64 - RtpJitter.SKEW_LARGE_GAP) || 
        (skew - this.skewAvg > RtpJitter.SKEW_LARGE_GAP)) {
        this.resetSkew(rxTimestamp, rtpTimestamp)
        deltaSend = 0u64
        deltaRecv = 0u64
        skew = 0i64
    }
    
    // 滑动窗口处理
    if (this.windowSize == 0u32) {
        // 初始化阶段
        let earlyReturnOpt = this.computeSkewInitialPhase(rxTimestamp, skew)
        if (earlyReturnOpt.isSome()) {
            this.resetSkew(rxTimestamp, rtpTimestamp)
            outTimestamp = match (earlyReturnOpt) {
                case Some(ts) => ts
                case None => rxTimestamp
            }
            return outTimestamp
        }
    } else {
        // 滑动阶段
        this.computeSkewSlidingPhase(skew)
    }
    
    // 估计的输出时间戳
    outTimestamp = this.firstRxTimestamp + deltaSend + UInt64(this.skewAvg)
    
    // 确保不会倒退
    if (outTimestamp + UInt64(this.cfg.delay) < rxTimestamp) {
        this.resetSkew(rxTimestamp, rtpTimestamp)
        outTimestamp = rxTimestamp
    }
    
    return outTimestamp
}

关键算法

  1. 滑动窗口:使用固定大小的滑动窗口存储偏移量样本,计算最小偏移量作为基准
  2. 指数移动平均:使用指数移动平均算法平滑偏移量,减少抖动影响
  3. 大间隙检测:检测到大的偏移量变化时,重置偏移量计算,适应网络变化
  4. 时钟漂移补偿:通过比较接收时间戳和 RTP 时间戳,检测并补偿时钟漂移

3.5 包排序和去重

挑战:网络乱序和重复包需要正确处理,确保数据按顺序、无重复地处理。

解决方案:实现按序列号排序的插入算法:

cangjie 复制代码
// 将包加入抖动缓冲
public func enqueue(pkt: RtpPkt): Unit {
    let inTimestamp = pkt.inTimestamp
    let rtpTimestamp = pkt.rtpTimestamp
    
    if (this.firstRxTimestamp == 0u64 || this.firstRtpTimestamp == 0u64) {
        this.resetSkew(inTimestamp, rtpTimestamp)
    }
    
    if (this.lastRxTimestamp != 0u64 && this.lastRtpTimestamp != 0u64) {
        this.computeJitter(inTimestamp, rtpTimestamp)
    }
    pkt.outTimestamp = this.computeSkew(inTimestamp, rtpTimestamp)
    
    this.lastRxTimestamp = inTimestamp
    this.lastRtpTimestamp = rtpTimestamp
    
    // 检查是否是旧包或重复包
    let diff = rtpDiffSeqnum(this.nextSeqnum, pkt.header.seqnum)
    if (Int16(diff) > 0i16) {
        // 旧包或已经处理过的重复包
        return
    }
    
    // 按序列号顺序插入
    var inserted = false
    var i: Int64 = 0
    while (i < this.packets.size && !inserted) {
        let itemOpt = this.packets.get(i)
        if (itemOpt.isSome()) {
            let item = match (itemOpt) {
                case Some(p) => p
                case None => RtpPkt()
            }
            let itemDiff = rtpDiffSeqnum(item.header.seqnum, pkt.header.seqnum)
            let diffVal = Int16(itemDiff)
            if (diffVal > 0i16) {
                // 继续查找
            } else if (diffVal == 0i16) {
                // 重复包
                return
            } else {
                // 按顺序插入
                this.insertPacketAtPosition(pkt, i)
                inserted = true
            }
        }
        i = i + 1
    }
    
    // 空列表或当前包要作为第一个添加
    if (!inserted) {
        this.appendPacketToFront(pkt)
    }
}

四、性能优化

4.1 字节序转换优化

优化策略:使用位操作和移位运算,避免多次内存访问:

cangjie 复制代码
// 优化的 32 位整数读取
private func readUInt32(data: Array<UInt8>, pos: Int64): Option<UInt32> {
    if (pos + 3i64 >= data.size) {
        return None
    }
    let b0 = data[Int(pos)]
    let b1 = data[Int(pos + 1i64)]
    let b2 = data[Int(pos + 2i64)]
    let b3 = data[Int(pos + 3i64)]
    let value = (UInt32(b0) << 24u32) | (UInt32(b1) << 16u32) | (UInt32(b2) << 8u32) | UInt32(b3)
    // 网络字节序是大端序,需要转换为主机字节序
    let hostValue = ((value & 0x000000FFu32) << 24u32) | 
                    ((value & 0x0000FF00u32) << 8u32) | 
                    ((value & 0x00FF0000u32) >> 8u32) | 
                    ((value & 0xFF000000u32) >> 24u32)
    return Some(hostValue)
}

4.2 内存管理优化

优化策略:使用 ArrayList 动态分配内存,避免频繁的内存分配和释放:

cangjie 复制代码
// 使用 ArrayList 构建字节数组
private func rtpPktWriteHeader(data: ArrayList<UInt8>, header: RtpPktHeader): Unit {
    writeUInt16(data, header.flags)
    writeUInt16(data, header.seqnum)
    writeUInt32(data, header.timestamp)
    writeUInt32(data, header.ssrc)
}

4.3 抖动缓冲优化

优化策略:使用滑动窗口和指数移动平均,减少计算复杂度:

cangjie 复制代码
// 滑动窗口处理
private func computeSkewSlidingPhase(skew: Int64): Unit {
    // 记住旧值并设置新值
    var old: Int64 = 0i64
    if (this.windowPos < UInt32(RtpJitter.SKEW_WINDOW_MAX_SIZE)) {
        match (this.window.get(Int64(this.windowPos))) {
            case Some(v) => old = v
            case None => old = 0i64
        }
        this.window[Int64(this.windowPos)] = skew
    }
    
    if (skew < this.windowMin) {
        // 找到新的最小值
        this.windowMin = skew
    } else if (old == this.windowMin) {
        // 我们替换了当前的最小值,找到新的最小值
        this.findNewWindowMin(old)
    }
    
    // 更新位置并在需要时回绕
    this.windowPos = this.windowPos + 1u32
    if (this.windowPos >= this.windowSize) {
        this.windowPos = 0u32
    }
    
    // 滑动平均
    this.skewAvg = this.skewAvg + (this.windowMin - this.skewAvg) / RtpJitter.SKEW_AVG_ALPHA
}

五、测试策略

5.1 单元测试

librtp 库提供了完整的单元测试,覆盖所有核心功能:

cangjie 复制代码
@TestCase
func testRtpPktReadBasic(): Unit {
    // 构造一个基本的RTP包(12字节头部 + 4字节负载,网络字节序)
    let data: Array<UInt8> = [
        0x60u8, 0x80u8,  // flags: 版本2, 无填充, 无扩展, CSRC=0, 标记=0, 负载类型=96
        0x39u8, 0x30u8,  // 序列号 12345 (网络字节序)
        0x32u8, 0x09u8, 0x01u8, 0x00u8,  // 时间戳 67890 (网络字节序)
        0x78u8, 0x56u8, 0x34u8, 0x12u8,  // SSRC 0x12345678 (网络字节序)
        0x01u8, 0x02u8, 0x03u8, 0x04u8   // 负载数据
    ]
    
    let pktOpt = rtpPktRead(data)
    @Assert(pktOpt.isSome(), true)
    let pkt = match (pktOpt) {
        case Some(p) => p
        case None => RtpPkt()
    }
    @Assert(pkt.header.seqnum, 12345u16)
    @Assert(pkt.header.timestamp, 67890u32)
    @Assert(pkt.header.ssrc, 0x12345678u32)
}

5.2 集成测试

集成测试验证模块间的协作:

cangjie 复制代码
@TestCase
func testRtpJitterProcess(): Unit {
    let cfg = RtpJitterCfg(90000u32, 20000u32)
    let cbs = RtpJitterCbs()
    let jitter = RtpJitter(cfg, cbs, None)
    
    // 创建并入队多个包
    for (var i = 0u16; i < 10u16; i = i + 1u16) {
        let pkt = rtpPktNew()
        pkt.header.seqnum = i
        pkt.header.timestamp = UInt32(i) * 90000u32
        pkt.inTimestamp = UInt64(i) * 1000000u64
        pkt.rtpTimestamp = UInt64(i) * 90000u64
        jitter.enqueue(pkt)
    }
    
    // 处理包
    jitter.process(12000000u64)
    
    // 验证结果
    let (packetCount, byteCount, size) = jitter.getInfo()
    @Assert(packetCount, 0u32)  // 所有包应该都已处理
}

六、使用示例

6.1 基本 RTP 包处理

cangjie 复制代码
import librtp_cj.*

main() {
    // 创建 RTP 包
    let pkt = rtpPktNew()
    pkt.header.seqnum = 12345u16
    pkt.header.timestamp = 67890u32
    pkt.header.ssrc = 0x12345678u32
    
    // 设置负载数据
    pkt.rawData = [0x01u8, 0x02u8, 0x03u8, 0x04u8]
    pkt.payloadOff = 0i64
    pkt.payloadLen = 4i64
    
    // 序列化 RTP 包
    let dataOpt = rtpPktFinalizeHeader(pkt)
    match (dataOpt) {
        case Some(data) => {
            println("RTP 包序列化成功,大小: " + data.size.toString() + " 字节")
            // 发送 data 到网络
        }
        case None => {
            println("RTP 包序列化失败")
        }
    }
}

6.2 RTCP 包处理

cangjie 复制代码
import librtp_cj.*

main() {
    // 创建 RTCP 发送者报告
    let sr = RtcpPktSenderReport()
    sr.ssrc = 0x12345678u32
    sr.ntpTimestamp.seconds = 1234567890u32
    sr.ntpTimestamp.fraction = 0x40000000u32
    sr.rtpTimestamp = 67890u32
    sr.senderPacketCount = 1000u32
    sr.senderByteCount = 50000u32
    
    // 序列化 RTCP 包
    let dataOpt = rtcpPktWriteSenderReport(sr)
    match (dataOpt) {
        case Some(data) => {
            println("RTCP SR 包序列化成功,大小: " + data.size.toString() + " 字节")
            // 发送 data 到网络
        }
        case None => {
            println("RTCP SR 包序列化失败")
        }
    }
    
    // 解析 RTCP 包
    let cbs = RtcpPktReadCbs()
    cbs.senderReport = Some(func(sr: RtcpPktSenderReport): Unit {
        println("收到 SR 包,SSRC: 0x" + sr.ssrc.toString(16))
    })
    let success = rtcpPktRead(data, cbs)
    if (success) {
        println("RTCP 包解析成功")
    }
}

6.3 抖动缓冲使用

cangjie 复制代码
import librtp_cj.*

main() {
    // 创建抖动缓冲配置
    let cfg = RtpJitterCfg(90000u32, 20000u32)  // 时钟频率 90000 Hz,延迟 20 ms
    
    // 创建回调
    let cbs = RtpJitterCbs()
    cbs.processPkt = Some(func(jitter: RtpJitter, pkt: RtpPkt, gap: UInt32): Unit {
        println("处理包,序列号: " + pkt.header.seqnum.toString() + ", gap: " + gap.toString())
        // 处理包数据
    })
    
    // 创建抖动缓冲
    let jitter = RtpJitter(cfg, cbs, None)
    
    // 从网络接收包并入队
    while (hasMorePackets()) {
        let data = receivePacket()
        let pktOpt = rtpPktRead(data)
        match (pktOpt) {
            case Some(pkt) => {
                pkt.inTimestamp = getCurrentTimestamp()  // 接收时间戳(微秒)
                pkt.rtpTimestamp = UInt64(pkt.header.timestamp)
                jitter.enqueue(pkt)
            }
            case None => {
                println("RTP 包读取失败")
            }
        }
    }
    
    // 定期处理包
    while (true) {
        let curTimestamp = getCurrentTimestamp()
        jitter.process(curTimestamp)
        sleep(10)  // 休眠 10 毫秒
    }
}

七、常见问题

Q1: 如何处理网络字节序转换?

A: librtp 库在读取和写入时自动进行字节序转换,无需手动处理。读取时会将网络字节序转换为主机字节序,写入时会将主机字节序转换为网络字节序。

Q2: 如何设置 RTP 包的负载类型?

A: 使用 rtpPktHeaderFlagsSet() 函数设置负载类型:

cangjie 复制代码
let flags = rtpPktHeaderFlagsSet(pkt.header.flags, "PAYLOAD", 96u32)  // 设置负载类型为 96
pkt.header.flags = flags

Q3: 如何处理 RTP 扩展头?

A: 设置扩展标志并设置扩展头信息:

cangjie 复制代码
// 设置扩展标志
let flags = rtpPktHeaderFlagsSet(pkt.header.flags, "EXTENSION", 1u32)
pkt.header.flags = flags

// 设置扩展头 ID 和长度
pkt.extHeaderId = 0xBEDEu16
pkt.extHeaderLen = 8i64  // 扩展头长度(包括 4 字节头部)

Q4: 如何计算 RTP 时间戳?

A: 根据时钟频率和采样时间计算:

cangjie 复制代码
// 时钟频率 90000 Hz,采样时间 1 秒
let clkRate = 90000u32
let sampleTime = 1.0  // 秒
let rtpTimestamp = UInt64(sampleTime * Float64(clkRate))

Q5: 抖动缓冲的延迟如何设置?

A: 根据网络条件和应用需求设置延迟:

cangjie 复制代码
// 网络条件较好,延迟可以设置较小(10-20 ms)
let cfg = RtpJitterCfg(90000u32, 10000u32)  // 延迟 10 ms

// 网络条件较差,延迟需要设置较大(50-100 ms)
let cfg = RtpJitterCfg(90000u32, 50000u32)  // 延迟 50 ms

Q6: 如何处理 RTCP 复合包?

A: rtcpPktRead() 函数会自动处理复合包,依次解析每个 RTCP 包并调用相应的回调函数:

cangjie 复制代码
let cbs = RtcpPktReadCbs()
cbs.senderReport = Some(func(sr: RtcpPktSenderReport): Unit {
    // 处理 SR 包
})
cbs.receiverReport = Some(func(rr: RtcpPktReceiverReport): Unit {
    // 处理 RR 包
})
let success = rtcpPktRead(data, cbs)  // 自动处理复合包

Q7: 如何设置库的输出类型?

A: 在 cjpm.toml 中设置 output-type

  • static:静态库(推荐用于库项目)
  • dynamic:动态库
  • executable:可执行程序(用于测试)

Q8: HLT 和 LLT 的区别是什么?

A:

  • LLT(Low Level Test):低层测试,关注单个函数、类的正确性,类似单元测试
  • HLT(High Level Test):高层测试,关注模块间协作、端到端功能,类似集成测试

Q9: 如何管理依赖库?

A: 在 cjpm.toml[dependencies] 部分添加依赖:

toml 复制代码
[dependencies]
  other-lib = "1.0.0"

Q10: 如何发布不同版本?

A:

  1. 修改 cjpm.toml 中的 version
  2. 更新 CHANGELOG.md 记录变更
  3. 打标签并推送到仓库
  4. 提交到官方三方库平台

相关资源

仓颉标准库:https://gitcode.com/Cangjie/cangjie_runtime/tree/main/stdlib

仓颉扩展库:https://gitcode.com/Cangjie/cangjie_stdx

仓颉命令行工具:https://gitcode.com/Cangjie/cangjie_tools

仓颉语言测试用例:https://gitcode.com/Cangjie/cangjie_test

仓颉语言示例代码:https://gitcode.com/Cangjie/Cangjie-Examples

仓颉鸿蒙示例应用:https://gitcode.com/Cangjie/HarmonyOS-Examples

精品三方库:https://gitcode.com/org/Cangjie-TPC/repos

SIG 孵化库:https://gitcode.com/org/Cangjie-SIG/repos


日期 :2025年11月
版本:1.0.0

本文基于 librtp 库的实际开发经验编写,如有任何问题或建议,欢迎在项目仓库中提出 Issue 或 Pull Request。

相关推荐
爱睡觉的咋3 天前
探索仓颉编程语言:从Hello World到性能实战
仓颉
特立独行的猫a5 天前
仓颉编程语言的并发编程:线程模型与使用实践
华为·线程·仓颉·仓颉语言
L.EscaRC5 天前
仓颉(Cangjie)语言之网络编程浅析
网络·仓颉
在人间负债^7 天前
从Python到仓颉:核心项目内容迁移实践
开发语言·python·鸿蒙·仓颉
superman超哥9 天前
仓颉语言中流式I/O的设计模式深度剖析
开发语言·后端·设计模式·仓颉
特立独行的猫a11 天前
仓颉语言宏(Cangjie Macros)详细介绍及强大使用
华为··鸿蒙·仓颉·仓颉语言
晨陌y14 天前
深入剖析:仓颉语言的性能优化核心技术
android·性能优化·仓颉
七夜zippoe14 天前
仓颉语言核心特性深度解析——现代编程范式的集大成者
开发语言·后端·鸿蒙·鸿蒙系统·仓颉
superman超哥14 天前
仓颉语言中正则表达式引擎的深度剖析与实践
开发语言·后端·仓颉