1 基础知识回归
1.1 RTMP
一、协议基础
-
用途
- 用于实时音视频流传输(如直播、点播),支持低延迟。
- 基于 TCP 协议,保证可靠传输。
-
传输单元: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:无任何头信息(完全复用)。
-
-
多路复用
- 通过 Chunk Stream ID (CSID) 区分不同流(如音频、视频、控制信息)。
- 常见 CSID:
0-63
:单字节 Basic Header(最常用)。64-319
:双字节 Basic Header。320-65599
:三字节 Basic Header。
二、连接与握手
-
握手流程
- C0+C1(客户端 → 服务器):版本号 + 时间戳/随机数据。
- S0+S1+S2(服务器 → 客户端):版本号 + 时间戳/随机数据 + 回显 C1。
- C2(客户端 → 服务器):回显 S1。
- 完成后进入数据传输阶段。
-
协议变体
- RTMP:标准协议,明文传输。
- RTMPS:基于 TLS/SSL 的安全版本。
- RTMPT:通过 HTTP 隧道传输,穿透防火墙。
三、消息类型
-
控制消息
SET_CHUNK_SIZE
(4):动态调整 chunkSize。ABORT_MESSAGE
(5):中止未完成的消息。ACKNOWLEDGEMENT
(3):确认已接收的字节数。
-
媒体消息
AUDIO
(8):音频数据。VIDEO
(9):视频数据。AMF Metadata
(18):元数据(如视频分辨率、码率)。
-
命令消息
invoke
(20):远程方法调用(如connect
、play
、publish
)。onStatus
:状态通知(如NetStream.Play.Start
)。
四、关键机制
-
时间戳处理
- 绝对时间戳:首个 Chunk 使用(Type 0)。
- 相对时间戳:后续 Chunk 使用差值(Type 1/2)。
- 扩展时间戳:当时间戳超过 24 位时使用(4 字节)。
-
数据分块规则
- chunkSize 限制每个 Chunk 的 Data 部分长度(默认 128 字节,可动态调整)。
- 消息长度 > chunkSize 时,需拆分为多个 Chunk(首个 Type 0,后续 Type 3)。
-
QoS 机制
- 通过
Window Acknowledgement Size
控制流量(确认窗口大小)。 SET_PEER_BANDWIDTH
限制对端发送速率。
- 通过
1.2 WEBRTC
好的!以下是 WebRTC(Web Real-Time Communication)的核心关键点总结:
一、协议基础
-
定义
- 浏览器原生支持的实时音视频通信技术(无需插件)。
- 基于 UDP 协议,通过 SRTP(安全实时传输协议)保证安全。
-
三大核心 API
- MediaStream API :获取摄像头/麦克风流(
getUserMedia
)。 - RTCPeerConnection API:建立 P2P 连接,传输音视频数据。
- RTCDataChannel API:传输任意数据(如文本、文件)。
- MediaStream API :获取摄像头/麦克风流(
-
信令流程
-
用于交换元数据(如 SDP、ICE 候选者),需自建信令服务器(如 Socket.IO、WebSocket)。
-
典型流程 :
客户端A → 信令服务器 → 客户端B (Offer → Answer → ICE 候选者)
-
二、关键组件
-
SDP(会话描述协议)
-
描述媒体类型(音频/视频)、编解码格式、网络参数等。
-
示例字段 :
m=audio 9 RTP/AVP 111 103 9 ... (媒体类型、端口、编码) a=rtpmap:111 opus/48000/2 (opus 编码,48kHz,双声道)
-
-
ICE(交互式连接建立)
- 穿越 NAT 和防火墙,寻找最佳连接路径。
- 候选者类型 :
- 主机候选者(Host Candidate):本地 IP:Port。
- 服务器反射候选者(SRFLX):通过 STUN 服务器获取的公网 IP:Port。
- 中继候选者(RELAY):通过 TURN 服务器中转的地址。
-
STUN/TURN 服务器
- STUN(会话遍历网络地址转换):获取公网 IP 和端口。
- TURN(中继用户数据报协议):当 P2P 连接失败时,作为中继服务器转发数据。
三、数据传输
-
媒体传输
- 音频编码:Opus(默认)、G.711。
- 视频编码:VP8/VP9(Chrome/Firefox)、H.264(Safari/Edge)。
- 自动适应网络:根据丢包率动态调整分辨率/码率。
-
数据通道(Data Channel)
- 基于 SCTP 协议,支持可靠(Ordered)或不可靠(Unordered)传输。
- 应用场景:实时聊天、文件共享、游戏状态同步。
2 RTMP to WEBRTC-srs版
一、协议转换原理
- RTMP 协议特性
基于 TCP:可靠传输,但延迟较高(1-3 秒)。
消息格式:音频 / 视频数据封装在 FLV 格式中,通过 Chunk 分块传输。
推拉模式:推流端主动发送数据,拉流端被动接收。 - 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):cppuint32_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 推流和拉流
- 推流 使用ffmpeg推流
bash
ffmpeg -re -i ./doc/source.flv -vcodec copy -acodec copy -f flv -y rtmp://192.168.203.117/live/livestream
- 拉流端
bash
http://192.168.203.117/:8080/players/rtc_player.html