【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在复杂网络环境下的高质量、低延迟音视频通信体验。

相关推荐
Fisher3Star18 小时前
WebRTC Transport 两种创建方式的差异解析
webrtc
Fisher3Star21 小时前
FFmpeg推流至Mediasoup全流程指南
webrtc
Fisher3Star1 天前
mediasoup 创建Router全流程详解
webrtc
声网1 天前
OpenAI 的 WebRTC 秘密架构:没有 SFU?没有问题!丨 Voice Agent 学习笔记
学习·架构·webrtc
HySpark5 天前
VAD 与流式 ASR 踩坑复盘及完整解决方案
webrtc·vad·离线语音转写·流式asr·qwen-asr·音频预处理
徐子元竟然被占了!!5 天前
WebRTC协议
webrtc
ZC跨境爬虫5 天前
跟着 MDN 学 HTML day_28:(使用选择器 API 在 DOM 树中进行选择与遍历)
前端·ui·html·音视频·webrtc
Fisher3Star12 天前
mediasoup Transport详解与代码实现
webrtc
Fisher3Star12 天前
mediasoup中Node.js与Worker进程通信机制
网络·webrtc
911hzh13 天前
Flutter WebRTC iOS 原理解析:从 getUserMedia 到 Texture,讲清视频采集、纹理渲染与远端通话链路
flutter·ios·webrtc