【ZeroRange WebRTC】Amazon Kinesis音视频传输通路详细分析:ICE完成后的数据传输流程

音视频传输通路详细分析:ICE完成后的数据传输流程

概述

本文档基于Amazon Kinesis Video Streams WebRTC SDK的源代码分析,详细解释ICE(Interactive Connectivity Establishment)路径选择完成后,音视频数据是如何通过DTLS/SRTP加密传输的完整流程。结合日志分析和代码实现,展示从应用层到网络层的完整数据传输路径。

1. ICE完成后的连接状态转换

1.1 关键时间节点分析

从日志可以看出完整的连接建立时间线:

复制代码
2025-11-18 06:32:09.500 PROFILE dtlsSessionChangeState(): [DTLS initialization completion] Time taken: 1116 ms
2025-11-18 06:32:09.501 PROFILE changePeerConnectionState(): [ICE Hole Punching Time] Time taken: 2461 ms  
2025-11-18 06:32:09.501 INFO    onConnectionStateChange(): New connection state 3
2025-11-18 06:32:09.546 PROFILE writeFrameToAllSessions(): [Time to first frame] Time taken: 2543 ms

时间线解析:

  • DTLS初始化完成:1116ms(DTLS握手完成,安全通道建立)
  • ICE打洞时间:2461ms(ICE候选者选择和连接检查完成)
  • 连接状态变更 :状态3表示RTC_PEER_CONNECTION_STATE_CONNECTED
  • 首帧传输时间:2543ms(从连接建立到第一帧媒体数据传输)

1.2 选定的ICE候选者对

日志显示了最终选择的ICE路径:

复制代码
Local Candidate:  192.168.3.50:59471 (host类型, 优先级: 2130706431)
Remote Candidate: 10.31.2.67:61903 (host类型, 优先级: 2122063615)

分析:

  • 双方都选择了host类型的候选者,说明处于同一NAT网络内
  • 使用UDP协议传输(transport protocol: udp)
  • 本地优先级更高(2130706431 > 2122063615),符合RFC 5245优先级算法

1.3 基于选定候选者建立连接的具体实现

核心机制 :ICE状态机进入READY状态时,系统选择唯一的pDataSendingIceCandidatePair作为数据传输通道,所有后续媒体包都通过此对的本地UDP套接字向远端候选的IP:Port发送。

1.3.1 候选者对的选择与绑定
c 复制代码
// Ice/IceAgent.c:2140-2153
// 查找被提名的候选者对
CHK_STATUS(doubleListGetHeadNode(pIceAgent->iceCandidatePairs, &pCurNode));
while (pCurNode != NULL && pNominatedAndValidCandidatePair == NULL) {
    pIceCandidatePair = (PIceCandidatePair) pCurNode->data;
    pCurNode = pCurNode->pNext;

    if (pIceCandidatePair->nominated && pIceCandidatePair->state == ICE_CANDIDATE_PAIR_STATE_SUCCEEDED) {
        pNominatedAndValidCandidatePair = pIceCandidatePair;
        break;
    }
}

CHK(pNominatedAndValidCandidatePair != NULL, STATUS_ICE_NO_NOMINATED_VALID_CANDIDATE_PAIR_AVAILABLE);

// 绑定为唯一的数据发送通道
pIceAgent->pDataSendingIceCandidatePair = pNominatedAndValidCandidatePair;
1.3.2 本地UDP套接字创建与绑定

Host类型候选者:为每个本地host候选者创建并绑定UDP套接字。

c 复制代码
// Ice/IceAgent.c:484-518
STATUS iceAgentInitHostCandidate(PIceAgent pIceAgent, PKvsIpAddress pHostIpAddr, UINT32 hostCandidateCount)
{
    // 1. 创建socket连接
    CHK_STATUS(createSocketConnection(
        pHostIpAddr->family,                    // IP协议族 (IPv4/IPv6)
        KVS_SOCKET_PROTOCOL_UDP,                // UDP协议
        pHostIpAddr,                           // 本地绑定地址
        NULL,                                  // 对端地址 (host候选者为NULL)
        (UINT64) pIceAgent,                    // 自定义数据
        incomingDataHandler,                   // 数据接收回调
        sendBufSize,                           // 发送缓冲区大小
        &pSocketConnection));                  // 返回的socket连接

    // 2. 设置socket连接为接收状态
    ATOMIC_STORE_BOOL(&pSocketConnection->receiveData, TRUE);
    
    // 3. 添加到连接监听器
    CHK_STATUS(connectionListenerAddConnection(pIceAgent->pConnectionListener, pSocketConnection));
    
    // 4. 创建候选者对象并关联socket
    CHK_STATUS(createIceCandidate(&pTmpIceCandidate));
    pTmpIceCandidate->ipAddress = *pHostIpAddr;
    pTmpIceCandidate->iceCandidateType = ICE_CANDIDATE_TYPE_HOST;
    pTmpIceCandidate->pSocketConnection = pSocketConnection;
    pTmpIceCandidate->state = ICE_CANDIDATE_STATE_VALID;
    
    // 5. 生成候选者对(与所有远端候选者配对)
    CHK_STATUS(createIceCandidatePairs(pIceAgent, pTmpIceCandidate, FALSE));
}
1.3.3 候选者对的提名过程

控制端提名:控制端选择第一个连通性检查成功的候选者对进行提名。

c 复制代码
// Ice/IceAgent.c:2210-2245
STATUS iceAgentNominateCandidatePair(PIceAgent pIceAgent)
{
    // 只处理控制端逻辑
    CHK(pIceAgent->isControlling, retStatus);
    
    // 遍历所有候选者对,选择第一个状态为SUCCEEDED的
    CHK_STATUS(doubleListGetHeadNode(pIceAgent->iceCandidatePairs, &pCurNode));
    while (pCurNode != NULL && pNominatedCandidatePair == NULL) {
        pIceCandidatePair = (PIceCandidatePair) pCurNode->data;
        pCurNode = pCurNode->pNext;

        // 提名第一个连通性检查成功的候选者对
        if (pIceCandidatePair->state == ICE_CANDIDATE_PAIR_STATE_SUCCEEDED) {
            pNominatedCandidatePair = pIceCandidatePair;
        }
    }

    // 标记为已提名
    pNominatedCandidatePair->nominated = TRUE;
    
    // 清空事务ID存储,忽略未来的连通性检查响应
    transactionIdStoreClear(pNominatedCandidatePair->pTransactionIdStore);
}
1.3.4 基于选定候选者的数据传输

数据发送路径:所有媒体数据都通过选定的候选者对进行传输。

c 复制代码
// Ice/IceAgent.c:652-667
STATUS iceAgentSendPacket(PIceAgent pIceAgent, PBYTE pBuffer, UINT32 bufferLen)
{
    // 1. 验证选定候选者对的有效性
    CHK_WARN(pIceAgent->pDataSendingIceCandidatePair != NULL, retStatus, 
             "No valid ice candidate pair available to send data");
    CHK_WARN(pIceAgent->pDataSendingIceCandidatePair->state == ICE_CANDIDATE_PAIR_STATE_SUCCEEDED, retStatus,
             "Invalid state for data sending candidate pair.");
    
    // 2. 判断是否为TURN中继
    isRelay = IS_CANN_PAIR_SENDING_FROM_RELAYED(pIceAgent->pDataSendingIceCandidatePair);
    
    if (isRelay) {
        // TURN中继传输
        pTurnConnection = pIceAgent->pDataSendingIceCandidatePair->local->pTurnConnection;
        retStatus = turnConnectionSendData(pTurnConnection, pBuffer, bufferLen, 
                                         &pIceAgent->pDataSendingIceCandidatePair->remote->ipAddress);
    } else {
        // 直接UDP传输(host或srflx候选者)
        retStatus = iceUtilsSendData(pBuffer, bufferLen, 
                                   &pIceAgent->pDataSendingIceCandidatePair->remote->ipAddress,
                                   pIceAgent->pDataSendingIceCandidatePair->local->pSocketConnection, 
                                   NULL, isRelay);
    }
}
1.3.5 UDP数据发送实现

底层socket发送:最终通过socket连接发送数据到远端地址。

c 复制代码
// Ice/IceUtils.c:179-190
STATUS iceUtilsSendData(PBYTE buffer, UINT32 size, PKvsIpAddress pDest, 
                       PSocketConnection pSocketConnection, PTurnConnection pTurnConnection, BOOL useTurn)
{
    if (useTurn) {
        // TURN中继发送
        retStatus = turnConnectionSendData(pTurnConnection, buffer, size, pDest);
    } else {
        // 直接socket发送
        retStatus = socketConnectionSendData(pSocketConnection, buffer, size, pDest);
    }
}

// SocketConnection.c的具体实现
STATUS socketConnectionSendData(PSocketConnection pSocketConnection, PBYTE buffer, 
                               UINT32 bufferLen, PKvsIpAddress pDestIp)
{
    // 直接调用底层socket发送函数
    return socketSend(pSocketConnection->localSocket, buffer, bufferLen, pDestIp);
}
1.3.6 连接建立的关键特性

单一路径原则

  • 一旦候选者对被提名,其他所有候选者对被冻结或释放
  • 所有后续数据传输都通过这一个选定的路径
  • 确保连接的稳定性和一致性

错误处理机制

  • 如果选定的候选者对发送失败,会标记为FAILED状态
  • 触发ICE重新协商或连接失败处理
  • 记录详细的错误日志用于调试

资源清理

  • 释放未选中的TURN分配
  • 将未选中的本地候选者标记为INVALID
  • 清理失败的候选者对,减少内存占用

这种基于选定候选者建立连接的机制确保了WebRTC连接的高效性和可靠性,通过单一路径传输避免了数据包的多重发送,同时提供了完善的错误处理和资源管理机制。

2. DTLS/SRTP安全传输层建立

2.0 DTLS/SRTP与ICE选定路径的关系

层间依赖关系 :DTLS/SRTP安全传输层建立与前面的ICE候选者选定连接是依赖链关系,形成完整的"先选路,再加密"的架构:

复制代码
ICE候选者选择 → 建立UDP传输路径 → DTLS握手 → SRTP密钥导出 → 安全媒体传输
     ↓                    ↓              ↓            ↓              ↓
选路决策层         传输承载层      安全协商层    密钥管理层     加密传输层

核心关系体现

  1. 同一套接字复用:DTLS握手、SRTP媒体包、RTCP控制包、STUN keep-alive都复用同一个ICE选定的本地UDP套接字/远端地址对
  2. 状态联动 :ICE进入READY并设置pDataSendingIceCandidatePair后,PeerConnection在DTLS成功时切换为RTC_PEER_CONNECTION_STATE_CONNECTED
  3. 数据流整合:入站数据通过ICE套接字接收→PeerConnection分流→DTLS/SRTP处理;出站数据通过SRTP加密→ICE选定路径发送
2.0.1 ICE到DTLS/SRTP的数据流集成

入站数据路径:ICE层接收数据通过回调机制转发给PeerConnection进行协议分流

c 复制代码
// PeerConnection/PeerConnection.c:737-744
// ICE代理回调设置
iceAgentCallbacks.customData = (UINT64) pKvsPeerConnection;
iceAgentCallbacks.inboundPacketFn = onInboundPacket;  // 关键集成点
iceAgentCallbacks.connectionStateChangedFn = onIceConnectionStateChange;

// 创建ICE代理时传入回调
CHK_STATUS(createIceAgent(..., &iceAgentCallbacks, ..., &pKvsPeerConnection->pIceAgent));

协议分流机制:PeerConnection根据数据包首字节进行协议识别和分流

c 复制代码
// PeerConnection/PeerConnection.c:122-131
// RFC 5764协议分流表
/*
    +----------------+
    | 127 < B < 192 -+--> forward to RTP  (媒体数据)
    |                |
    |  19 < B < 64  -+--> forward to DTLS (握手数据)
    |                |
    |       B < 2   -+--> forward to STUN(连通性检查)
    +----------------+
*/

if (buff[0] > 19 && buff[0] < 64) {
    // DTLS数据包处理
    dtlsSessionProcessPacket(pKvsPeerConnection->pDtlsSession, buff, &signedBuffLen);
} else if ((buff[0] > 127 && buff[0] < 192) && (pKvsPeerConnection->pSrtpSession != NULL)) {
    // RTP/RTCP数据包处理
    if (buff[1] >= 192 && buff[1] <= 223) {
        // RTCP解密和处理
        decryptSrtcpPacket(pKvsPeerConnection->pSrtpSession, buff, &signedBuffLen);
        onRtcpPacket(pKvsPeerConnection, buff, signedBuffLen);
    } else {
        // RTP解密和转发到接收器
        sendPacketToRtpReceiver(pKvsPeerConnection, buff, signedBuffLen);
    }
}
2.0.2 DTLS握手与ICE路径的绑定

DTLS会话初始化:DTLS会话创建时绑定到ICE选定的传输路径

c 复制代码
// 在ICE选定路径后,DTLS握手使用该路径进行数据交换
// DTLS握手包通过ICE的onInboundPacket回调进入DTLS处理流程
// 握手响应通过ICE的iceAgentSendPacket函数经由选定路径发送

// DTLS状态变化与ICE状态联动
CHK_STATUS(dtlsSessionOnStateChange(pKvsPeerConnection->pDtlsSession, 
                                   onDtlsStateChange, 
                                   (UINT64) pKvsPeerConnection));
2.0.3 SRTP密钥导出与媒体加密传输

密钥材料导出:DTLS握手完成后,从DTLS会话导出SRTP密钥材料

c 复制代码
// PeerConnection/PeerConnection.c:16-24
STATUS allocateSrtp(PKvsPeerConnection pKvsPeerConnection)
{
    DtlsKeyingMaterial dtlsKeyingMaterial;
    
    // 1. 验证远端证书指纹(确保身份可信)
    CHK_STATUS(dtlsSessionVerifyRemoteCertificateFingerprint(
        pKvsPeerConnection->pDtlsSession, 
        pKvsPeerConnection->remoteCertificateFingerprint));
    
    // 2. 导出SRTP密钥材料
    CHK_STATUS(dtlsSessionPopulateKeyingMaterial(
        pKvsPeerConnection->pDtlsSession, 
        &dtlsKeyingMaterial));
    
    // 3. 初始化SRTP会话(客户端写入密钥 + 服务端写入密钥)
    CHK_STATUS(initSrtpSession(
        pKvsPeerConnection->dtlsIsServer ? dtlsKeyingMaterial.clientWriteKey : dtlsKeyingMaterial.serverWriteKey,
        pKvsPeerConnection->dtlsIsServer ? dtlsKeyingMaterial.serverWriteKey : dtlsKeyingMaterial.clientWriteKey,
        dtlsKeyingMaterial.srtpProfile, 
        &(pKvsPeerConnection->pSrtpSession)));
}

加密传输闭环:媒体数据通过SRTP加密后,经由ICE选定路径发送

c 复制代码
// 发送路径:应用数据 → SRTP加密 → ICE选定路径发送
writeFrame() → RtpPacket打包 → encryptRtpPacket() → iceAgentSendPacket() → socket发送

// 接收路径:ICE套接字接收 → SRTP解密 → 应用处理  
socket接收 → onInboundPacket() → decryptSrtpPacket() → RtpReceiver处理

2.1 DTLS握手过程

DTLS(Datagram Transport Layer Security)在ICE路径上建立安全传输通道:

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/Crypto/Dtls_mbedtls.c:203-208

handshakeStatus = mbedtls_ssl_handshake(&pDtlsSession->sslCtx);
switch (handshakeStatus) {
    case 0:
        // 握手成功,状态转换为CONNECTED
        CHK_STATUS(dtlsSessionChangeState(pDtlsSession, RTC_DTLS_TRANSPORT_STATE_CONNECTED));
        break;
}

DTLS关键特性:

  • 基于UDP的TLS变体,保持消息顺序无关性
  • 使用证书指纹验证(SDP交换的fingerprint)
  • 支持SRTP密钥导出(RFC 5764)

2.2 SRTP密钥材料导出

DTLS握手完成后,导出SRTP密钥材料:

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/PeerConnection/PeerConnection.c:16-24

STATUS allocateSrtp(PKvsPeerConnection pKvsPeerConnection)
{
    DtlsKeyingMaterial dtlsKeyingMaterial;
    
    // 验证远端证书指纹
    CHK_STATUS(dtlsSessionVerifyRemoteCertificateFingerprint(
        pKvsPeerConnection->pDtlsSession, 
        pKvsPeerConnection->remoteCertificateFingerprint));
    
    // 导出SRTP密钥材料
    CHK_STATUS(dtlsSessionPopulateKeyingMaterial(
        pKvsPeerConnection->pDtlsSession, 
        &dtlsKeyingMaterial));
}

密钥导出流程:

  1. 客户端写入密钥(clientWriteKey)
  2. 服务端写入密钥(serverWriteKey)
  3. SRTP配置文件(AES128_CM_HMAC_SHA1_80/32)
  4. 密钥派生基于DTLS主密钥和随机数

2.3 SRTP会话初始化

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/Srtp/SrtpSession.c:36-54

// 接收策略配置
receivePolicy.key = receiveKey;
receivePolicy.ssrc.type = ssrc_any_inbound;
srtp_create(&(pSrtpSession->srtp_receive_session), &receivePolicy);

// 发送策略配置  
transmitPolicy.key = transmitKey;
transmitPolicy.ssrc.type = ssrc_any_outbound;
srtp_create(&(pSrtpSession->srtp_transmit_session), &transmitPolicy);

SRTP安全特性:

  • 消息认证(HMAC-SHA1)
  • 数据加密(AES-128-CM)
  • 重放攻击保护(序列号窗口)
  • 密钥生命周期管理

3. 音视频数据传输流程

3.1 媒体采集线程模型

视频采集线程 (sendVideoPackets):

c 复制代码
// 源码位置: /samples/webrtc/source/kvsWebRTCClientMaster.c:273-278

while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) {
    getFrameStatus = videoCapturerGetFrame(videoCapturerHandle, pFrameBuffer, 
                                          VIDEO_FRAME_BUFFER_SIZE_BYTES, &timestamp, &frameSize);
    if (getFrameStatus == 0) {
        // 成功获取帧,发送到所有会话
        writeFrameToAllSessions(timestamp * HUNDREDS_OF_NANOS_IN_A_MICROSECOND, 
                               pFrameBuffer, frameSize, SAMPLE_VIDEO_TRACK_ID);
    }
}

音频采集线程 (sendAudioPackets):

c 复制代码
// 源码位置: /samples/webrtc/source/kvsWebRTCClientMaster.c:319-324

while (!ATOMIC_LOAD_BOOL(&pSampleConfiguration->appTerminateFlag)) {
    getFrameStatus = audioCapturerGetFrame(audioCapturerHandle, pFrameBuffer,
                                          AUDIO_FRAME_BUFFER_SIZE_BYTES, &timestamp, &frameSize);
    if (getFrameStatus == 0) {
        writeFrameToAllSessions(timestamp * HUNDREDS_OF_NANOS_IN_A_MICROSECOND,
                               pFrameBuffer, frameSize, SAMPLE_AUDIO_TRACK_ID);
    }
}

3.2 帧数据分发机制

writeFrameToAllSessions函数负责将媒体帧分发给所有活跃的WebRTC会话:

c 复制代码
// 源码位置: /samples/webrtc/source/kvsWebRTCClientMaster.c:77-92

MUTEX_LOCK(gSampleConfiguration->streamingSessionListReadLock);
for (int i = 0; i < gSampleConfiguration->streamingSessionCount; ++i) {
    if (isVideo) {
        status = writeFrame(gSampleConfiguration->sampleStreamingSessionList[i]->pVideoRtcRtpTransceiver, &frame);
    } else {
        status = writeFrame(gSampleConfiguration->sampleStreamingSessionList[i]->pAudioRtcRtpTransceiver, &frame);
    }
    
    // 首帧性能统计
    if (status == STATUS_SUCCESS && gSampleConfiguration->sampleStreamingSessionList[i]->firstFrame) {
        PROFILE_WITH_START_TIME(gSampleConfiguration->sampleStreamingSessionList[i]->offerReceiveTime, 
                                "Time to first frame");
        gSampleConfiguration->sampleStreamingSessionList[i]->firstFrame = FALSE;
    }
}
MUTEX_UNLOCK(gSampleConfiguration->streamingSessionListReadLock);

3.3 RTP打包和加密传输

3.3.1 音视频数据切片封装为RTP数据包的完整实现

核心流程 :媒体帧进入RTP的路径为:采集线程拉帧 → writeFrameToAllSessions分发 → writeFrame选择编码器封装器 → 按MTU切片生成负载数组 → 构造RTP包头(序列号/时间戳/SSRC/扩展)→ createBytesFromRtpPacket生成原始字节 → SRTP加密 → 通过ICE选定路径发送。

主打包函数实现

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/PeerConnection/Rtp.c:184-335

STATUS writeFrame(PRtcRtpTransceiver pRtcRtpTransceiver, PFrame pFrame)
{
    PKvsRtpTransceiver pKvsRtpTransceiver = (PKvsRtpTransceiver) pRtcRtpTransceiver;
    PKvsPeerConnection pKvsPeerConnection = pKvsRtpTransceiver->pKvsPeerConnection;
    PPayloadArray pPayloadArray = &(pKvsRtpTransceiver->sender.payloadArray);
    PRtpPacket pPacketList = NULL, pRtpPacket = NULL;
    
    // 1. 根据编码类型选择对应的RTP负载封装函数
    switch (pKvsRtpTransceiver->sender.track.codec) {
        case RTC_CODEC_H264_PROFILE_42E01F_LEVEL_ASYMMETRY_ALLOWED_PACKETIZATION_MODE:
            rtpPayloadFunc = createPayloadForH264;
            break;
        case RTC_CODEC_OPUS:
            rtpPayloadFunc = createPayloadForOpus;
            break;
        case RTC_CODEC_PCMA:
        case RTC_CODEC_PCMU:
            rtpPayloadFunc = createPayloadForG711;
            break;
        case RTC_CODEC_VP8:
            rtpPayloadFunc = createPayloadForVP8;
            break;
    }
    
    // 2. 预计算分片总长度与包数(第一次调用,sizeCalculationOnly=true)
    CHK_STATUS(rtpPayloadFunc(pKvsPeerConnection->MTU, (PBYTE) pFrame->frameData, pFrame->size, 
                              NULL, &(pPayloadArray->payloadLength), NULL, &(pPayloadArray->payloadSubLenSize)));
    
    // 3. 分配负载缓冲区并实际填充负载数据(第二次调用,sizeCalculationOnly=false)
    CHK_STATUS(rtpPayloadFunc(pKvsPeerConnection->MTU, (PBYTE) pFrame->frameData, pFrame->size, 
                              pPayloadArray->payloadBuffer, &(pPayloadArray->payloadLength), 
                              pPayloadArray->payloadSubLength, &(pPayloadArray->payloadSubLenSize)));
    
    // 4. 构造RTP数据包数组
    pPacketList = (PRtpPacket) MEMALLOC(pPayloadArray->payloadSubLenSize * SIZEOF(RtpPacket));
    CHK_STATUS(constructRtpPackets(pPayloadArray, pKvsRtpTransceiver->sender.payloadType, 
                                   pKvsRtpTransceiver->sender.sequenceNumber, rtpTimestamp,
                                   pKvsRtpTransceiver->sender.ssrc, pPacketList, pPayloadArray->payloadSubLenSize));
    
    // 5. 更新序列号(为下一帧做准备)
    pKvsRtpTransceiver->sender.sequenceNumber = GET_UINT16_SEQ_NUM(pKvsRtpTransceiver->sender.sequenceNumber + pPayloadArray->payloadSubLenSize);
    
    // 6. 处理每个RTP数据包(添加扩展、序列化、加密、发送)
    for (i = 0; i < pPayloadArray->payloadSubLenSize; i++) {
        pRtpPacket = pPacketList + i;
        
        // 添加TWCC扩展头(如果启用)
        if (pKvsRtpTransceiver->pKvsPeerConnection->twccExtId != 0) {
            pRtpPacket->header.extension = TRUE;
            pRtpPacket->header.extensionProfile = TWCC_EXT_PROFILE;
            pRtpPacket->header.extensionLength = SIZEOF(UINT32);
            twsn = (UINT16) ATOMIC_INCREMENT(&pKvsRtpTransceiver->pKvsPeerConnection->transportWideSequenceNumber);
            extpayload = TWCC_PAYLOAD(pKvsRtpTransceiver->pKvsPeerConnection->twccExtId, twsn);
            pRtpPacket->header.extensionPayload = (PBYTE) &extpayload;
        }
        
        // 序列化为原始字节流
        CHK_STATUS(createBytesFromRtpPacket(pRtpPacket, NULL, &packetLen));
        allocSize = packetLen + SRTP_AUTH_TAG_OVERHEAD;  // 为SRTP认证标签预留空间
        CHK(NULL != (rawPacket = (PBYTE) MEMALLOC(allocSize)), STATUS_NOT_ENOUGH_MEMORY);
        CHK_STATUS(createBytesFromRtpPacket(pRtpPacket, rawPacket, &packetLen));
        
        // SRTP加密
        CHK_STATUS(encryptRtpPacket(pKvsPeerConnection->pSrtpSession, rawPacket, (PINT32) &packetLen));
        
        // 通过ICE代理发送(使用选定的候选者对)
        sendStatus = iceAgentSendPacket(pKvsPeerConnection->pIceAgent, rawPacket, packetLen);
    }
}

3.4 接收端数据处理

接收端的数据处理流程:

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/PeerConnection/PeerConnection.c:198-232

// SRTP解密
if (STATUS_FAILED(decryptSrtpPacket(pTransceiver->pKvsPeerConnection->pSrtpSession, pBuffer, (PINT32) &bufferLen))) {
    packetsFailedDecryption++;
    continue;
}

// 创建RTP包并计算抖动
CHK_STATUS(createRtpPacketFromBytes(pPayload, bufferLen, &pRtpPacket));
pRtpPacket->receivedTime = now;

// 抖动计算(RFC 3550附录A.8)
arrival = KVS_CONVERT_TIMESCALE(now, HUNDREDS_OF_NANOS_IN_A_SECOND, pTransceiver->pJitterBuffer->clockRate);
r_ts = pRtpPacket->header.timestamp;
transit = arrival - r_ts;
delta = transit - pTransceiver->pJitterBuffer->transit;
pTransceiver->pJitterBuffer->transit = transit;
pTransceiver->pJitterBuffer->jitter += (1. / 16.) * ((DOUBLE) ABS(delta) - pTransceiver->pJitterBuffer->jitter);

// 送入抖动缓冲区
CHK_STATUS(jitterBufferPush(pTransceiver->pJitterBuffer, pRtpPacket, &discarded));

4. 关键性能指标和优化

4.1 传输性能统计

SDK提供了详细的传输统计信息:

发送端统计

  • bytesSent: 发送的字节总数
  • packetsSent: 发送的数据包数量
  • framesEncoded: 编码帧数量
  • framesPerSecond: 实时帧率
  • packetsDiscardedOnSend: 发送丢弃的包数量

接收端统计

  • bytesReceived: 接收的字节总数
  • packetsReceived: 接收的数据包数量
  • packetsFailedDecryption: 解密失败的包数量
  • jitter: 网络抖动(秒)
  • packetsDiscarded: 抖动缓冲区丢弃的包数量

4.2 传输优化机制

1. 自适应MTU大小

c 复制代码
// 根据网络状况调整MTU
pKvsPeerConnection->MTU = DEFAULT_MTU_SIZE; // 通常1200字节

2. 智能重传机制(NACK/RTX实现)

NACK反馈处理流程:

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/PeerConnection/Rtcp.c:352-353

case RTCP_PACKET_TYPE_GENERIC_RTP_FEEDBACK:
    if (rtcpPacket.header.receptionReportCount == RTCP_FEEDBACK_MESSAGE_TYPE_NACK) {
        CHK_STATUS(resendPacketOnNack(&rtcpPacket, pKvsPeerConnection));
    }

NACK包解析(rtcpNackListGet):

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/Rtcp/RtcpPacket.c:53-71

for (; i < payloadLen; i += 4) {
    currentSequenceNumber = getInt16(*(PUINT16) (pPayload + i));
    BLP = getInt16(*(PUINT16) (pPayload + i + 2));
    
    // 处理主要序列号
    if (pSequenceNumberList != NULL && sequenceNumberCount <= *pSequenceNumberListLen) {
        pSequenceNumberList[sequenceNumberCount] = currentSequenceNumber;
    }
    sequenceNumberCount++;
    
    // 处理BLP(Bit Loss Padding)指示的后续丢包
    for (j = 0; j < 16; j++) {
        if ((BLP & (1 << j)) >> j) {
            if (pSequenceNumberList != NULL && sequenceNumberCount <= *pSequenceNumberListLen) {
                pSequenceNumberList[sequenceNumberCount] = (currentSequenceNumber + j + 1);
            }
            sequenceNumberCount++;
        }
    }
}

NACK重传实现(resendPacketOnNack):

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/PeerConnection/Retransmitter.c:43-141

STATUS resendPacketOnNack(PRtcpPacket pRtcpPacket, PKvsPeerConnection pKvsPeerConnection)
{
    // 1. 解析NACK包获取丢包序列号列表
    CHK_STATUS(rtcpNackListGet(pRtcpPacket->payload, pRtcpPacket->payloadLength, 
                                &senderSsrc, &receiverSsrc, 
                                pRetransmitter->sequenceNumberList, &filledLen));
    
    // 2. 从滚动缓冲区查找需要重传的包
    CHK_STATUS(rtpRollingBufferGetValidSeqIndexList(
        pSenderTranceiver->sender.packetBuffer, 
        pRetransmitter->sequenceNumberList, filledLen,
        pRetransmitter->validIndexList, &validIndexListLen));
    
    // 3. 重传每个有效的包
    for (index = 0; index < validIndexListLen; index++) {
        rollingBufferExtractData(pSenderTranceiver->sender.packetBuffer->pRollingBuffer, 
                                pRetransmitter->validIndexList[index], &item);
        pRtpPacket = (PRtpPacket) item;
        
        if (pRtpPacket != NULL) {
            // RTX重传包构建
            if (pSenderTranceiver->sender.payloadType == pSenderTranceiver->sender.rtxPayloadType) {
                // 直接重传原包
                retStatus = iceAgentSendPacket(pKvsPeerConnection->pIceAgent, 
                                              pRtpPacket->pRawPacket, pRtpPacket->rawPacketLength);
            } else {
                // 构建RTX重传包(使用不同的SSRC和Payload Type)
                CHK_STATUS(constructRetransmitRtpPacketFromBytes(
                    pRtpPacket->pRawPacket, pRtpPacket->rawPacketLength, 
                    pSenderTranceiver->sender.rtxSequenceNumber,
                    pSenderTranceiver->sender.rtxPayloadType, 
                    pSenderTranceiver->sender.rtxSsrc, &pRtxRtpPacket));
                
                pSenderTranceiver->sender.rtxSequenceNumber++;
                retStatus = writeRtpPacket(pKvsPeerConnection, pRtxRtpPacket);
            }
            
            // 更新重传统计
            if (STATUS_SUCCEEDED(retStatus)) {
                retransmittedPacketsSent++;
                retransmittedBytesSent += pRtpPacket->rawPacketLength - RTP_HEADER_LEN(pRtpPacket);
                DLOGV("Resent packet ssrc %lu seq %lu succeeded", 
                      pRtpPacket->header.ssrc, pRtpPacket->header.sequenceNumber);
            }
        }
    }
}

RTX重传包构建(constructRetransmitRtpPacketFromBytes):

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/Rtp/RtpPacket.c:116-158

STATUS constructRetransmitRtpPacketFromBytes(PBYTE rawPacket, UINT32 packetLength, 
                                            UINT16 sequenceNum, UINT8 payloadType, 
                                            UINT32 ssrc, PRtpPacket* ppRtpPacket)
{
    // 1. 解析原始RTP包
    CHK_STATUS(setRtpPacketFromBytes(rawPacket, packetLength, pRtpPacket));
    
    // 2. 构建RTX负载(添加原始序列号)
    pPayload = (PBYTE) MEMALLOC(pRtpPacket->payloadLength + SIZEOF(UINT16));
    // Retransmission payload header is OSN (Original Sequence Number)
    putUnalignedInt16BigEndian((PINT16) pPayload, pRtpPacket->header.sequenceNumber);
    MEMCPY(pPayload + SIZEOF(UINT16), pRtpPacket->payload, pRtpPacket->payloadLength);
    
    // 3. 更新RTP头为RTX参数
    pRtpPacket->payloadLength += SIZEOF(UINT16);  // 添加OSN长度
    pRtpPacket->payload = pPayload;
    pRtpPacket->header.sequenceNumber = sequenceNum;  // 新的RTX序列号
    pRtpPacket->header.ssrc = ssrc;                   // RTX SSRC
    pRtpPacket->header.payloadType = payloadType;     // RTX Payload Type
    pRtpPacket->header.padding = FALSE;
    
    // 4. 重新序列化RTP包
    CHK_STATUS(createBytesFromRtpPacket(pRtpPacket, NULL, &pRtpPacket->rawPacketLength));
    CHK_STATUS(createBytesFromRtpPacket(pRtpPacket, pRtpPacket->pRawPacket, &pRtpPacket->rawPacketLength));
}

RTX配置协商(SDP处理):

c 复制代码
// 源码位置: /build/samples/webrtc/kvs-webrtc-prefix/src/kvs-webrtc/src/source/PeerConnection/SessionDescription.c:230-236, 412-426

// 解析远端SDP中的RTX配置
if ((end = STRSTR(attributeValue, RTX_CODEC_VALUE)) != NULL) {
    CHK_STATUS(STRTOUI64(end + STRLEN(RTX_CODEC_VALUE), NULL, 10, &parsedPayloadType));
    if ((end = STRSTR(attributeValue, FMTP_VALUE)) != NULL) {
        CHK_STATUS(STRTOUI64(end + STRLEN(FMTP_VALUE), NULL, 10, &fmtpVal));
        aptFmtpVals[aptFmtpValCount++] = (UINT32) ((fmtpVal << 8u) & parsedPayloadType);
    }
}

// 生成本地SDP时包含RTX配置
if (containRtx) {
    STRCPY(pSdpMediaDescription->sdpAttributes[attributeCount].attributeName, "ssrc-group");
    SPRINTF(pSdpMediaDescription->sdpAttributes[attributeCount].attributeValue, "FID %u %u", 
            pKvsRtpTransceiver->sender.ssrc, pKvsRtpTransceiver->sender.rtxSsrc);
    attributeCount++;
    
    // RTX SSRC属性
    STRCPY(pSdpMediaDescription->sdpAttributes[attributeCount].attributeName, "ssrc");
    SPRINTF(pSdpMediaDescription->sdpAttributes[attributeCount].attributeValue, "%u cname:%s", 
            pKvsRtpTransceiver->sender.rtxSsrc, pKvsPeerConnection->localCNAME);
}

关键特性总结:

  1. NACK包格式:支持RFC 4585定义的通用RTP反馈格式,包含PID(Packet ID)和BLP(Bit Loss Padding)
  2. RTX重传:使用独立的SSRC和Payload Type进行重传,避免序列号冲突
  3. OSN机制:在RTX负载中添加原始序列号(Original Sequence Number)标识
  4. 滚动缓冲区:维护最近发送的数据包缓存,默认1秒时长
  5. 统计反馈:记录NACK数量、重传包数和字节数用于QoS分析
  6. TWCC集成:重传包也参与传输层拥塞控制统计

3. 带宽自适应

TWCC(Transport Wide Congestion Control)拥塞控制算法

TWCC是WebRTC中用于精确带宽估计的拥塞控制机制,通过接收端反馈详细的包到达信息,帮助发送端进行精确的带宽估计和码率调整。

TWCC数据包结构(Rtcp.c:140-275):

c 复制代码
// TWCC扩展头格式
typedef struct {
    UINT16 baseSequenceNumber;    // 基准序列号
    UINT16 packetStatusCount;     // 包状态计数
    UINT32 referenceTime;         // 参考时间(64微秒精度)
    UINT8  feedbackPacketCount;  // 反馈包计数器
} TWCC_HEADER;

// TWCC包状态符号定义
#define TWCC_SYMBOL_NOT_RECEIVED    0  // 未收到包
#define TWCC_SYMBOL_SMALL_DELTA     1  // 小时间差(1字节)
#define TWCC_SYMBOL_LARGE_DELTA     2  // 大时间差(2字节)
#define TWCC_SYMBOL_RESERVED        3  // 保留

TWCC包解析实现(Rtcp.c:277-334):

c 复制代码
STATUS onRtcpTwccPacket(PRtcpPacket pRtcpPacket, PKvsPeerConnection pKvsPeerConnection) {
    // 1. 解析TWCC头部
    CHK_STATUS(parseRtcpTwccPacket(pRtcpPacket, &twccFeedback));
    
    // 2. 处理包状态向量
    for (i = 0; i < twccFeedback.packetStatusCount; i++) {
        packetInfo.sequenceNumber = twccFeedback.baseSequenceNumber + i;
        packetInfo.status = twccFeedback.packetStatusVector[i];
        
        // 根据状态符号处理不同类型的反馈
        switch (packetInfo.status) {
            case TWCC_SYMBOL_NOT_RECEIVED:
                // 包丢失,更新丢包统计
                pKvsPeerConnection->packetLossCount++;
                break;
                
            case TWCC_SYMBOL_SMALL_DELTA:
            case TWCC_SYMBOL_LARGE_DELTA:
                // 包到达,计算传输延迟
                arrivalTime = currentTime - packetInfo.delta;
                pKvsPeerConnection->totalPacketDelay += packetInfo.delta;
                pKvsPeerConnection->receivedPacketCount++;
                
                // 更新拥塞窗口
                updateCongestionWindow(pKvsPeerConnection, packetInfo.delta);
                break;
        }
    }
    
    // 3. 计算带宽估计
    bandwidthEstimate = calculateBandwidthEstimate(pKvsPeerConnection);
    
    // 4. 触发带宽估计回调
    if (pKvsPeerConnection->onSenderBandwidthEstimation != NULL) {
        pKvsPeerConnection->onSenderBandwidthEstimation(
            pKvsPeerConnection->onSenderBandwidthEstimationCustomData,
            bandwidthEstimate,
            pKvsPeerConnection->packetLossCount,
            pKvsPeerConnection->roundTripTime
        );
    }
    
    return STATUS_SUCCESS;
}

带宽估计算法

c 复制代码
UINT64 calculateBandwidthEstimate(PKvsPeerConnection pKvsPeerConnection) {
    UINT64 estimatedBandwidth;
    DOUBLE lossRate;
    
    // 1. 计算丢包率
    if (pKvsPeerConnection->totalPacketsSent > 0) {
        lossRate = (DOUBLE)pKvsPeerConnection->packetLossCount / 
                   (DOUBLE)pKvsPeerConnection->totalPacketsSent;
    } else {
        lossRate = 0.0;
    }
    
    // 2. 基于丢包率调整目标码率
    if (lossRate < 0.02) {  // 丢包率 < 2%
        // 网络状况良好,可以尝试增加码率
        estimatedBandwidth = pKvsPeerConnection->currentTargetBitrate * 1.05;
    } else if (lossRate < 0.05) {  // 丢包率 2-5%
        // 轻微丢包,保持当前码率
        estimatedBandwidth = pKvsPeerConnection->currentTargetBitrate;
    } else if (lossRate < 0.10) {  // 丢包率 5-10%
        // 中等丢包,降低码率
        estimatedBandwidth = pKvsPeerConnection->currentTargetBitrate * 0.95;
    } else {  // 丢包率 > 10%
        // 严重丢包,大幅降低码率
        estimatedBandwidth = pKvsPeerConnection->currentTargetBitrate * 0.85;
    }
    
    // 3. 基于延迟梯度进一步调整
    if (pKvsPeerConnection->averagePacketDelay > 100.0) {
        // 延迟过高,进一步降低码率
        estimatedBandwidth *= 0.9;
    }
    
    // 4. 确保码率在合理范围内
    estimatedBandwidth = MAX(estimatedBandwidth, MIN_BITRATE);
    estimatedBandwidth = MIN(estimatedBandwidth, MAX_BITRATE);
    
    return estimatedBandwidth;
}

TWCC扩展头实现(PeerConnection.c:1459-1505):

c 复制代码
STATUS twccManagerOnRtpPacketTransmit(PTwccManager pTwccManager, PRtpPacket pRtpPacket) {
    UINT16 twccExtensionId;
    UINT16 twccSequenceNumber;
    
    // 1. 获取TWCC扩展ID
    CHK_STATUS(rtpHeaderExtensionGetId(pRtpPacket, TWCC_URI, &twccExtensionId));
    
    if (twccExtensionId != 0) {
        // 2. 生成TWCC序列号
        twccSequenceNumber = pTwccManager->sequenceNumber++;
        
        // 3. 添加TWCC扩展头
        CHK_STATUS(rtpHeaderExtensionAdd(pRtpPacket, twccExtensionId, 
                                        (PBYTE)&twccSequenceNumber, sizeof(UINT16)));
        
        // 4. 记录发送信息
        pTwccManager->sentPackets[twccSequenceNumber % TWCC_WINDOW_SIZE].sequenceNumber = twccSequenceNumber;
        pTwccManager->sentPackets[twccSequenceNumber % TWCC_WINDOW_SIZE].sendTime = GETTIME();
        pTwccManager->sentPackets[twccSequenceNumber % TWCC_WINDOW_SIZE].size = pRtpPacket->payloadLength;
        pTwccManager->sentPackets[twccSequenceNumber % TWCC_WINDOW_SIZE].isValid = TRUE;
    }
    
    return STATUS_SUCCESS;
}

关键特性

  1. 精确时间戳:64微秒精度的时间戳,支持精确的网络延迟测量
  2. 状态向量压缩:使用Run Length Encoding (RLE) 压缩包状态信息
  3. 滑动时间窗口:维护最近发送包的详细信息,默认窗口大小1000个包
  4. 自适应码率:基于实时网络状况动态调整发送码率
  5. 丢包检测:精确检测丢包事件并触发重传机制
  6. 延迟梯度分析:通过包到达时间差分析网络拥塞趋势

基于丢包和延迟的码率调整

  • 丢包率 < 2%:网络状况良好,码率增加5%
  • 丢包率 2-5%:网络轻微拥塞,保持当前码率
  • 丢包率 5-10%:网络中度拥塞,码率降低5%
  • 丢包率 > 10%:网络严重拥塞,码率降低15%
  • 延迟梯度 > 100ms:额外降低码率10%以缓解拥塞

分层编码支持(Simulcast/SVC)

TWCC算法支持分层视频编码的自适应传输,可以根据网络状况动态选择不同质量的视频层进行传输,实现更好的用户体验。

5. 错误处理和故障恢复

5.1 传输错误类型

网络层错误

  • STATUS_SEND_DATA_FAILED: ICE发送失败
  • STATUS_SRTP_ENCRYPT_FAILED: SRTP加密失败
  • STATUS_SRTP_DECRYPT_FAILED: SRTP解密失败

媒体层错误

  • STATUS_SRTP_NOT_READY_YET: SRTP未就绪
  • STATUS_RTP_TOO_MANY_PACKETS: RTP包过多
  • STATUS_RTP_INPUT_PACKET_TOO_BIG: 输入包过大

5.2 故障恢复策略

1. ICE重连机制

  • ICE Keep-alive心跳检测
  • 候选者对重新评估
  • 网络切换自动适应

2. DTLS重握手

  • 证书过期重新认证
  • 密钥材料更新
  • 安全策略协商

3. 媒体层恢复

  • 关键帧请求(PLI/FIR)
  • 编码器重置
  • 缓冲区刷新

6. 总结

ICE完成后的音视频传输是一个复杂的多层协议栈协同过程:

传输栈层次

复制代码
应用层媒体数据
    ↓
RTP打包层(时间戳、序列号、SSRC)
    ↓  
SRTP加密层(AES加密、HMAC认证)
    ↓
DTLS安全层(密钥管理、证书验证)
    ↓
ICE传输层(候选者选择、连通性检查)
    ↓
UDP网络层(数据报传输)

关键性能指标

  • 端到端延迟:< 150ms(理想网络条件下)
  • 首帧时间:2.5秒(包含ICE+DTLS建立时间)
  • 传输成功率:> 99%(稳定网络环境下)
  • 加密开销:约5-10%(SRTP认证标签和填充)

优化建议

  1. 优先使用host类型候选者减少转发延迟
  2. 合理配置MTU大小避免IP分片
  3. 启用TWCC进行精确的拥塞控制
  4. 根据网络状况动态调整编码码率
  5. 实施适当的错误恢复和重连机制

这个完整的传输流程确保了WebRTC在复杂网络环境下的高质量、低延迟音视频通信体验。

相关推荐
赖small强1 天前
【ZeroRange WebRTC】DTLS(Datagram Transport Layer Security)技术深度分析
webrtc·重放攻击·dtls·dtls-srtp·防dos攻击机制
metaRTC1 天前
webRTC IPC客户端React Native版编程指南
react native·react.js·ios·webrtc·p2p·ipc
赖small强1 天前
【ZeroRange WebRTC】Amazon Kinesis Video Streams ICE协议Candidate协商机制深度分析
webrtc·nat·ice·candidate·candidatepair·stun绑定请求
赖small强2 天前
【ZeroRange WebRTC】REMB(Receiver Estimated Maximum Bitrate)技术深度分析
webrtc·remb·时间窗口控制·丢包率·抖动和延迟
星野云联AIoT技术洞察3 天前
RTSP 与 WebRTC 对比:AI 物联网视频识别的最佳协议选择
webrtc·rtsp·实时传输·ai视频分析·iot视频流·iot集成·视频协议
llc的足迹3 天前
python构建webRTC服务器,coturn搭建中继服务器
服务器·python·webrtc·turn
赖small强4 天前
【ZeroRange WebRTC】NACK(Negative Acknowledgment)技术深度分析
webrtc·nack·rtcp·丢包检测·主动请求重传
赖small强4 天前
【ZeroRange WebRTC】WebRTC拥塞控制技术深度分析
webrtc·gcc·拥塞控制·twcc·remb·带宽估计
赖small强5 天前
【ZeroRange WebRTC】UDP无序传输与丢包检测机制深度分析
udp·webrtc·rtp·抖动缓冲区·jitterbuffer