从基础到实战-rmpt to webrtc

1 基础知识回归

1.1 RTMP

一、协议基础

  1. 用途

    • 用于实时音视频流传输(如直播、点播),支持低延迟。
    • 基于 TCP 协议,保证可靠传输。
  2. 传输单元:Chunk

    • Chunk 结构

      复制代码
      Basic Header(1-3 字节) + Message Header(3-11 字节) + Extended Timestamp(可选) + Data(<= chunkSize 字节)
    • Chunk Type

      • Type 0:完整头信息(首次发送消息)。
      • Type 1:省略消息长度和类型(复用前一个消息)。
      • Type 2:仅时间戳差(复用 CSID、消息长度、类型)。
      • Type 3:无任何头信息(完全复用)。
  3. 多路复用

    • 通过 Chunk Stream ID (CSID) 区分不同流(如音频、视频、控制信息)。
    • 常见 CSID:
      • 0-63:单字节 Basic Header(最常用)。
      • 64-319:双字节 Basic Header。
      • 320-65599:三字节 Basic Header。

二、连接与握手

  1. 握手流程

    • C0+C1(客户端 → 服务器):版本号 + 时间戳/随机数据。
    • S0+S1+S2(服务器 → 客户端):版本号 + 时间戳/随机数据 + 回显 C1。
    • C2(客户端 → 服务器):回显 S1。
    • 完成后进入数据传输阶段。
  2. 协议变体

    • RTMP:标准协议,明文传输。
    • RTMPS:基于 TLS/SSL 的安全版本。
    • RTMPT:通过 HTTP 隧道传输,穿透防火墙。

三、消息类型

  1. 控制消息

    • SET_CHUNK_SIZE(4):动态调整 chunkSize。
    • ABORT_MESSAGE(5):中止未完成的消息。
    • ACKNOWLEDGEMENT(3):确认已接收的字节数。
  2. 媒体消息

    • AUDIO(8):音频数据。
    • VIDEO(9):视频数据。
    • AMF Metadata(18):元数据(如视频分辨率、码率)。
  3. 命令消息

    • invoke(20):远程方法调用(如 connectplaypublish)。
    • onStatus:状态通知(如 NetStream.Play.Start)。

四、关键机制

  1. 时间戳处理

    • 绝对时间戳:首个 Chunk 使用(Type 0)。
    • 相对时间戳:后续 Chunk 使用差值(Type 1/2)。
    • 扩展时间戳:当时间戳超过 24 位时使用(4 字节)。
  2. 数据分块规则

    • chunkSize 限制每个 Chunk 的 Data 部分长度(默认 128 字节,可动态调整)。
    • 消息长度 > chunkSize 时,需拆分为多个 Chunk(首个 Type 0,后续 Type 3)。
  3. QoS 机制

    • 通过 Window Acknowledgement Size 控制流量(确认窗口大小)。
    • SET_PEER_BANDWIDTH 限制对端发送速率。

1.2 WEBRTC

好的!以下是 WebRTC(Web Real-Time Communication)的核心关键点总结:

一、协议基础

  1. 定义

    • 浏览器原生支持的实时音视频通信技术(无需插件)。
    • 基于 UDP 协议,通过 SRTP(安全实时传输协议)保证安全。
  2. 三大核心 API

    • MediaStream API :获取摄像头/麦克风流(getUserMedia)。
    • RTCPeerConnection API:建立 P2P 连接,传输音视频数据。
    • RTCDataChannel API:传输任意数据(如文本、文件)。
  3. 信令流程

    • 用于交换元数据(如 SDP、ICE 候选者),需自建信令服务器(如 Socket.IO、WebSocket)。

    • 典型流程

      复制代码
      客户端A → 信令服务器 → 客户端B
      (Offer → Answer → ICE 候选者)

二、关键组件

  1. SDP(会话描述协议)

    • 描述媒体类型(音频/视频)、编解码格式、网络参数等。

    • 示例字段

      复制代码
      m=audio 9 RTP/AVP 111 103 9 ... (媒体类型、端口、编码)
      a=rtpmap:111 opus/48000/2 (opus 编码,48kHz,双声道)
  2. ICE(交互式连接建立)

    • 穿越 NAT 和防火墙,寻找最佳连接路径。
    • 候选者类型
      • 主机候选者(Host Candidate):本地 IP:Port。
      • 服务器反射候选者(SRFLX):通过 STUN 服务器获取的公网 IP:Port。
      • 中继候选者(RELAY):通过 TURN 服务器中转的地址。
  3. STUN/TURN 服务器

    • STUN(会话遍历网络地址转换):获取公网 IP 和端口。
    • TURN(中继用户数据报协议):当 P2P 连接失败时,作为中继服务器转发数据。

三、数据传输

  1. 媒体传输

    • 音频编码:Opus(默认)、G.711。
    • 视频编码:VP8/VP9(Chrome/Firefox)、H.264(Safari/Edge)。
    • 自动适应网络:根据丢包率动态调整分辨率/码率。
  2. 数据通道(Data Channel)

    • 基于 SCTP 协议,支持可靠(Ordered)或不可靠(Unordered)传输。
    • 应用场景:实时聊天、文件共享、游戏状态同步。

2 RTMP to WEBRTC-srs版

一、协议转换原理

  1. RTMP 协议特性
    基于 TCP:可靠传输,但延迟较高(1-3 秒)。
    消息格式:音频 / 视频数据封装在 FLV 格式中,通过 Chunk 分块传输。
    推拉模式:推流端主动发送数据,拉流端被动接收。
  2. WebRTC 协议特性
    基于 UDP:通过 SRTP 实现低延迟传输(<1 秒)。
    P2P 架构:直接连接客户端,需通过 STUN/TURN 穿透 NAT。
    媒体格式:音频常用 Opus,视频常用 VP8/VP9/H.264,通过 RTP 包传输。

从协议实现细节、类职责分工到数据流转的完整链路,以下是结合SRS源码和协议规范的深度解析:

二、协议转换的核心数据结构
1. RTMP 数据结构

cpp 复制代码
// SRS源码中的RTMP核心结构(简化版)
struct SrsRtmpMessage {
    int8_t type_id;       // 消息类型(8=音频,9=视频,18=元数据)
    int32_t timestamp;    // RTMP时间戳(毫秒)
    char* payload;        // FLV Tag数据
    int32_t payload_size; // 数据大小
};

struct SrsRtmpChunk {
    int8_t fmt;           // Chunk Type(0-3)
    int32_t csid;         // Chunk Stream ID
    char* header;         // 消息头(3-11字节)
    char* data;           // 数据(<= chunk_size)
};
  • 解析逻辑
    SrsRtmpProtocol::decode_chunk() 从TCP流中读取Chunk,根据fmt决定复用哪些头信息,重组完整的SrsRtmpMessage

2. WebRTC 数据结构

cpp 复制代码
// WebRTC核心结构(简化版)
struct RtpPacket {
    uint16_t sequence_number;  // RTP序列号(每包递增)
    uint32_t timestamp;        // RTP时间戳(采样时钟,如视频90kHz)
    uint8_t payload_type;      // 负载类型(如H.264=96, Opus=111)
    uint8_t* payload;          // 负载数据(如H.264 NAL单元)
    int payload_length;        // 负载长度
};

struct SdpMediaSection {
    std::string media_type;    // 媒体类型(audio/video)
    std::vector<std::string> codecs; // 支持的编码(如H.264/Opus)
    uint32_t clock_rate;       // 时钟频率(如视频90000Hz)
};
  • 封装逻辑
    RtpPacketizerH264::Packetize() 将H.264帧拆分为多个RTP包,每个包包含RTP头和分片后的NAL单元。

三、核心处理流程
1. RTMP 接收与解析
推流器(RTMP) SRS服务器 发送RTMP Chunk(Basic Header+Message Header+Data) SrsRtmpProtocol::decode_chunk()解析Chunk 解析11字节Message Header(绝对时间戳、消息长度等) 解析3字节时间戳差 alt [Chunk Type=0(完整头)] [Chunk Type=2(仅时间戳差)] 重组完整的SrsRtmpMessage 根据Message Type(8/9)分发到音频/视频处理逻辑 推流器(RTMP) SRS服务器

  • 关键函数
    SrsRtmpProtocol::do_cycle() 循环读取TCP数据,调用decode_chunk()process_message()

2. 媒体帧处理与存储

cpp 复制代码
// SRS处理RTMP视频帧的核心逻辑
int SrsRtmpConn::on_video_message(SrsRtmpMessage* msg) {
    // 解析FLV视频Tag头,获取编码类型(如H.264=7)
    uint8_t codec_id = (msg->payload[0] & 0xF0) >> 4;
    
    if (codec_id == SRS_RTMP_CODEC_H264) {
        // 解析H.264 NAL单元
        uint8_t avc_packet_type = msg->payload[1];
        uint32_t composition_time = SrsNalUnit::read_24be(msg->payload + 2);
        
        if (avc_packet_type == 0) { // 序列参数集(SPS)
            process_sps(msg->payload + 5, msg->payload_size - 5);
        } else if (avc_packet_type == 1) { // NAL单元数据
            // 提取NAL单元并存储到Stream
            srs_error_t err = stream->on_video_frame(msg->timestamp, 
                msg->payload + 5, msg->payload_size - 5);
        }
    }
    return srs_success;
}
  • 存储结构
    SrsStream 维护一个环形缓冲区,存储最近的音视频帧,供WebRTC模块拉取。

3. WebRTC 信令处理

cpp 复制代码
// SRS处理WebRTC Offer的核心逻辑
int SrsRtcServer::on_offer(std::string stream_name, std::string offer_sdp) {
    // 解析客户端Offer SDP
    SrsSdpParser parser;
    SrsSdpMessage* offer = parser.parse(offer_sdp);
    
    // 创建WebRTC会话
    SrsRtcSession* session = new SrsRtcSession(stream_name);
    
    // 生成Answer SDP(匹配编码、端口等参数)
    SrsSdpMessage* answer = session->create_answer(offer);
    
    // 返回Answer给客户端
    return send_answer(session->session_id(), answer->encode());
}
  • SDP关键参数
    • m=video 行指定视频编码(如VP8/VP9/H.264)。
    • a=rtpmap 指定编码参数(如a=rtpmap:96 H264/90000)。
    • a=fmtp 指定编码细节(如H.264的profile-level-id)。

4. RTP 打包与发送

cpp 复制代码
// H.264帧转RTP包的核心逻辑
int RtpPacketizerH264::Packetize(const uint8_t* payload, 
                                size_t payload_size,
                                std::vector<RtpPacket*>* packets) {
    // 解析H.264 NAL单元类型
    uint8_t nal_type = payload[0] & 0x1F;
    
    if (payload_size <= max_payload_size_) {
        // 单NAL单元包(Small NAL)
        RtpPacket* packet = create_packet();
        packet->AddPayload(payload, payload_size);
        packets->push_back(packet);
    } else {
        // 分片单元(FU-A)
        uint8_t fu_indicator = (payload[0] & 0xE0) | 28;  // FU-A NAL类型=28
        uint8_t fu_header = 0;
        
        // 设置开始位
        fu_header = (fu_header & 0x7F) | 0x80;
        // 创建第一个分片包
        RtpPacket* packet = create_packet();
        packet->AddPayload(&fu_indicator, 1);
        packet->AddPayload(&fu_header, 1);
        packet->AddPayload(payload + 1, max_payload_size_ - 2);
        packets->push_back(packet);
        
        // 中间分片包(无开始/结束标志)
        size_t offset = max_payload_size_ - 1;
        while (offset < payload_size - 1) {
            fu_header = (fu_header & 0x7F);  // 清除开始/结束标志
            // 创建中间分片包...
        }
        
        // 设置结束位
        fu_header = (fu_header & 0x7F) | 0x40;
        // 创建最后一个分片包...
    }
    return 0;
}
  • RTP时间戳转换
    RTMP时间戳(毫秒)→ RTP时间戳(视频90kHz,音频48kHz):

    cpp 复制代码
    uint32_t rtp_timestamp = rtmp_timestamp * 90; // 视频
    uint32_t rtp_timestamp = rtmp_timestamp * 48; // 音频

四、网络连接建立
1. ICE 候选收集与交换

cpp 复制代码
// SRS收集ICE候选的逻辑
int SrsIceAgent::GatherCandidates() {
    // 收集主机候选(本地IP和端口)
    std::vector<SrsIceCandidate> host_candidates = gather_host_candidates();
    
    // 通过STUN服务器获取反射候选
    if (stun_server_enabled_) {
        std::vector<SrsIceCandidate> srflx_candidates = 
            gather_server_reflexive_candidates(stun_server_);
        candidates_.insert(candidates_.end(), 
                          srflx_candidates.begin(), 
                          srflx_candidates.end());
    }
    
    // 如果需要,通过TURN服务器获取中继候选
    if (turn_server_enabled_) {
        std::vector<SrsIceCandidate> relay_candidates = 
            gather_relay_candidates(turn_server_);
        candidates_.insert(candidates_.end(), 
                          relay_candidates.begin(), 
                          relay_candidates.end());
    }
    
    // 将候选者发送给对端
    return send_ice_candidates(candidates_);
}
  • 优先级排序
    主机候选 > 反射候选 > 中继候选(根据RFC 5245算法计算优先级)。

2. DTLS 握手与加密
浏览器 SRS服务器 发送ClientHello(包含支持的加密套件) 选择加密套件(如TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) 发送ServerHello+证书+ServerKeyExchange 验证证书,生成预主密钥 发送ClientKeyExchange(包含预主密钥) 发送ChangeCipherSpec 发送ChangeCipherSpec 双方使用主密钥派生会话密钥 发送加密的Finished消息 发送加密的Finished消息 DTLS握手完成,开始加密通信 浏览器 SRS服务器

  • 数据加密
    使用SRTP(Secure RTP)对媒体数据加密,密钥从DTLS会话派生。

五、性能优化机制
1. 低延迟优化

  • 零拷贝设计
    SRS在可能的情况下避免内存拷贝(如SrsBuffer的引用计数机制)。
  • 小缓冲区
    内部缓冲区保持在3帧以内,减少排队延迟。
  • 快速丢弃策略
    网络拥塞时,优先丢弃旧帧而非等待,保证实时性。

2. 网络自适应

cpp 复制代码
// SRS根据网络状况调整码率的逻辑
int SrsRtcSender::on_rtcp_packet(SrsRtcpPacket* packet) {
    if (packet->type() == RTCP_RTPFB_NACK) {
        // 处理NACK丢包重传请求
        handle_nack((SrsRtcpNack*)packet);
    } else if (packet->type() == RTCP_RTPFB_REMB) {
        // 处理REMB带宽估计
        uint64_t bitrate_bps = ((SrsRtcpRemb*)packet)->bitrate();
        adjust_bitrate(bitrate_bps);
    }
    return srs_success;
}
  • 码率控制算法
    基于Google的REMB(Receiver Estimated Maximum Bitrate)和TWCC(Transport Wide Congestion Control)。

3 多协程

这上图清晰展示了 SRS 基于"单进程单线程 + 协程"的并发模型 ,核心是用协程模拟多任务,在单个线程内"分时复用"处理不同逻辑。

每个协程专注一类任务,逻辑上模拟"并行",实际在单线程内顺序执行(靠协程主动切换实现"并发"):

协程角色 核心工作内容 类比多线程中的角色
协程 1:监听连接 st_socket 监听端口(如 RTMP 的 1935、WebRTC 的 8080),accept 新连接后,创建新协程处理该连接。 主线程/监听线程
协程 2:处理 RTMP 推流 接收 RTMP 推流器(如 OBS)的 TCP 数据,解析 Chunk、FLV 格式,提取音视频帧存入缓冲区。 RTMP 业务线程
协程 3:处理 WebRTC 信令 处理 HTTP/WebSocket 请求(如 SDP 协商、ICE 候选交换),完成 WebRTC 连接建立前的"握手"。 信令线程
协程 4:转码 + RTP 打包 从缓冲区取 RTMP 帧,按需转码(如 H.265→H.264),再封装成 RTP 包(添加时间戳、序列号、负载类型)。 媒体处理线程(转码 + 协议转换)
协程 5:UDP 发送 RTP 从 RTP 缓冲区取包,通过 UDP 发送给 WebRTC 客户端;同时接收 RTCP 包,反馈丢包率、调整码率。 网络发送线程 + 质量反馈线程

六、总结:完整技术链路
TCP 视频 音频 WebSocket ICE候选 从Stream拉取帧 UDP RTCP反馈 RTMP推流 SrsRtmpProtocol解析Chunk SrsRtmpConn处理Message 消息类型 SrsH264Parser解析NAL单元 SrsAacParser解析ADTS帧 SrsStream存储视频帧 WebRTC客户端 SrsRtcServer接收Offer SrsSdpParser解析SDP SrsRtcSession创建Answer SrsIceAgent收集/验证候选 SrsRtpSender RtpPacketizerH264封装RTP包 SrsDtlsTransport加密数据 SrsIceTransport选择最优路径 SrsRtpReceiver SrsRtcSender调整码率/重传

总结 :转协议,无非就是把第一个协议封装的原始数据提取出来.然后就可以用第二种协议重新封装一下。

3 实战-基于srs

3.1 配置文件

cpp 复制代码
rtc_server {
    enabled on;
    listen 8000; # UDP port
    # @see https://ossrs.net/lts/zh-cn/docs/v4/doc/webrtc#config-candidate
    candidate 192.168.203.117;
}

vhost __defaultVhost__ {
    rtc {
        enabled     on;
        # @see https://ossrs.net/lts/zh-cn/docs/v4/doc/webrtc#rtmp-to-rtc
        rtmp_to_rtc on;
        # @see https://ossrs.net/lts/zh-cn/docs/v4/doc/webrtc#rtc-to-rtmp
        rtc_to_rtmp on;
    }
    http_remux {
        enabled     on;
        mount       [vhost]/[app]/[stream].flv;
    }
}

在 SRS 中,CANDIDATE 需配置为 客户端可访问的公网 IP 或域名,而非虚拟机 / WSL 的内网 IP。例如: 场景 1:若 SRS 运行在 云服务器(公网 IP),CANDIDATE 设为云服务器的公网 IP。

场景 2:若 SRS 运行在 本地虚拟机 / WSL,需先通过上述端口转发或内网穿透手段,将服务暴露到公网,然后将 $CANDIDATE 设为 宿主机的公网 IP 或 穿透工具分配的域名 / IP。

但是我这个实验是用内网ip做的 也就是说只能运行在本地 无需ice服务

3.2 推流和拉流

  1. 推流 使用ffmpeg推流
bash 复制代码
ffmpeg -re -i ./doc/source.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.203.117/live/livestream
  1. 拉流端
bash 复制代码
http://192.168.203.117/:8080/players/rtc_player.html
相关推荐
lul~1 小时前
[科研理论]无人机底层控制算法PID、LQR、MPC解析
c++·人工智能·无人机
我命由我123453 小时前
STM32 开发 - 中断案例(中断概述、STM32 的中断、NVIC 嵌套向量中断控制器、外部中断配置寄存器组、EXTI 外部中断控制器、实例实操)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
CodeWithMe3 小时前
【软件开发】上位机 & 下位机概念
c++
luofeiju4 小时前
数字图像处理与OpenCV初探
c++·图像处理·python·opencv·计算机视觉
whoarethenext4 小时前
使用 C/C++的OpenCV 将多张图片合成为视频
c语言·c++·opencv
weixin_428498494 小时前
Catch2 开源库介绍与使用指南
c++
freyazzr4 小时前
TCP/IP 网络编程 | Reactor事件处理模式
开发语言·网络·c++·网络协议·tcp/ip
小刘同学++5 小时前
用 OpenSSL 库实现 3DES(三重DES)加密
c++·算法·ssl
LunaGeeking6 小时前
重要的城市(图论 最短路)
c++·算法·编程·图论·最短路·floyd
君鼎6 小时前
C++内存管理与编译链接
c++