第29章:音视频网络传输——RTP/RTCP、RTMP、HLS与简易视频流服务器
音视频网络传输,说白了就是把摄像头拍到的一帧帧画面,加上麦克风录到的声音,打包扔到网络上,让另一端能流畅地看到、听到。这事儿看着简单,做起来坑不少。我早年做视频监控项目时,就被卡顿、花屏、音画不同步折磨得够呛。后来才明白,选对协议、理解协议背后的设计思想,比闷头写代码重要得多。
29.1 为什么需要专门的传输协议?
你想想看,HTTP传个文件,丢了包大不了重传,慢一点也无所谓。但视频不行——你正看着直播,突然画面卡住等重传,那体验就崩了。音视频对实时性要求极高,对丢包却有一定容忍度。所以,传统的TCP那套“保证可靠、顺序到达”的机制,反而成了累赘。
嗯,这里要记住一个核心原则:音视频传输,实时性优先于可靠性。丢一两个包,画面花一下,用户能忍;但卡顿几秒钟,用户直接关页面。
29.2 三大主流协议:RTP/RTCP、RTMP、HLS
目前业界用得最多的,就是这三兄弟。它们各有各的脾气,适用场景也不同。我一个个说。
29.2.1 RTP/RTCP:实时传输的基石
RTP(Real-time Transport Protocol)是真正的“传输工”。它负责把音视频数据切成小包,加上时间戳、序列号等信息,扔给UDP发出去。RTCP(RTP Control Protocol)则是“监工”,负责统计丢包率、延迟、抖动等质量参数,让发送端能动态调整码率。
我在项目中遇到过一个问题:用RTP传H.264视频,解码端总是花屏。查了半天,发现是RTP的负载类型(Payload Type)没配对。H.264的PT值是96,我写成了0,解码器根本不认。这种细节,文档里写得清清楚楚,但一上手就容易忽略。
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 |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
RTCP的SR(Sender Report)包也很关键,它包含了发送端的NTP时间戳和RTP时间戳的对应关系。接收端靠这个来同步音视频。我曾经调试音画不同步问题,就是发现RTCP的SR包没按时发送,导致接收端时间戳映射出错。
29.2.2 RTMP:直播界的“老将”
RTMP(Real-Time Messaging Protocol)是Adobe搞出来的,基于TCP。它最大的优点是低延迟——端到端能做到1-3秒。早期直播平台几乎清一色用RTMP。但它也有硬伤:基于TCP,握手复杂,而且Adobe Flash Player已经凉了。
不过,RTMP作为推流协议,至今仍是主流。很多编码器、推流软件(比如OBS)都支持RTMP推流。服务器收到RTMP流后,再转成HLS或WebRTC分发给用户。
RTMP消息格式:
// RTMP Chunk Header (基本头)
// 0: fmt (2 bits) + cs id (6 bits)
// 1-2: timestamp (3 bytes, 大端)
// 3-5: message length (3 bytes, 大端)
// 6: message type id (1 byte)
// 7-10: message stream id (4 bytes, 小端)
// 一个典型的视频数据消息
// fmt=0, cs_id=4, timestamp=0x123456
// message_length=0x001000 (4096字节)
// message_type_id=0x09 (Video)
// message_stream_id=0x00000001
29.2.3 HLS:苹果的“切片”方案
HLS(HTTP Live Streaming)是苹果推出的。它把视频切成一个个小片段(通常是2-10秒的TS文件),通过HTTP分发。客户端下载一个m3u8索引文件,然后按顺序下载TS片段播放。延迟比RTMP高(通常10-30秒),但兼容性极好——所有现代浏览器都支持,而且能轻松穿透防火墙。
HLS的核心是m3u8文件。它是个文本文件,里面列出了所有TS片段的URL和时长。服务器端需要不断生成新的m3u8文件和TS片段,客户端则定期刷新m3u8,获取新的片段。
一个简单的m3u8示例:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.000,
segment_000.ts
#EXTINF:10.000,
segment_001.ts
#EXTINF:10.000,
segment_002.ts
#EXT-X-ENDLIST
-segment_time 10强制固定时长,同时加上-break_non_keyframes 0确保每个片段从关键帧开始。
29.3 协议对比与选型
选哪个协议,得看你的场景。我整理了一张表,方便你对比:
| 协议 | 传输层 | 延迟 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| RTP/RTCP | UDP | 低(<1s) | 实时性好,支持组播 | 实现复杂,需处理NAT穿透 | 视频会议、VoIP、实时监控 |
| RTMP | TCP | 中(1-3s) | 低延迟,生态成熟 | 基于TCP,握手复杂,Flash已死 | 直播推流、互动直播 |
| HLS | HTTP/TCP | 高(10-30s) | 兼容性好,穿透防火墙 | 延迟高,切片有额外开销 | 点播、直播(非互动场景) |
29.4 简易视频流服务器实战
光说不练假把式。我们来写一个最简单的视频流服务器,用RTP传输H.264裸流。这个服务器从文件读取H.264数据,封装成RTP包,通过UDP发送给客户端。
核心思路:
- 读取H.264文件,找到NAL单元(以0x00000001或0x000001开头)
- 每个NAL单元封装成一个或多个RTP包(注意MTU限制,通常1500字节)
- 设置RTP头部:版本号=2,负载类型=96(H.264),序列号递增,时间戳递增
- 通过UDP socket发送
代码示例(简化版,仅展示核心逻辑):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define RTP_HEADER_SIZE 12
#define MAX_PAYLOAD_SIZE 1400 // 留点空间给IP/UDP头部
#define RTP_PT_H264 96
// RTP头部结构
typedef struct {
unsigned char csrccount:4;
unsigned char extension:1;
unsigned char padding:1;
unsigned char version:2;
unsigned char payloadtype:7;
unsigned char marker:1;
unsigned short seq;
unsigned int timestamp;
unsigned int ssrc;
} __attribute__((packed)) rtp_header_t;
// 发送RTP包
void send_rtp_packet(int sock, struct sockaddr_in *addr,
unsigned char *data, int len,
unsigned short *seq, unsigned int *ts) {
unsigned char packet[RTP_HEADER_SIZE + MAX_PAYLOAD_SIZE];
rtp_header_t *header = (rtp_header_t *)packet;
// 填充RTP头部
header->version = 2;
header->padding = 0;
header->extension = 0;
header->csrccount = 0;
header->marker = 0; // 非最后一包
header->payloadtype = RTP_PT_H264;
header->seq = htons((*seq)++);
header->timestamp = htonl(*ts);
header->ssrc = htonl(0x12345678);
// 拷贝数据
memcpy(packet + RTP_HEADER_SIZE, data, len);
// 发送
sendto(sock, packet, RTP_HEADER_SIZE + len, 0,
(struct sockaddr *)addr, sizeof(*addr));
}
int main(int argc, char *argv[]) {
// 省略:创建UDP socket、设置目标地址
// 省略:打开H.264文件、读取NAL单元
unsigned short seq = 0;
unsigned int timestamp = 0;
// 假设nal_data指向一个NAL单元,nal_len为其长度
// 每个NAL单元增加90000/30 = 3000时间戳(假设30fps)
send_rtp_packet(sock, &addr, nal_data, nal_len, &seq, ×tamp);
timestamp += 3000;
return 0;
}
29.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你看一眼,心里就有谱了。
这张图从应用场景往下,依次是协议选择、传输层、实现细节。你写代码时,就按这个层次来思考:先定场景,再选协议,最后扣细节。
29.6 写在最后
音视频传输这块,理论说破天也不如动手写一次。我建议你从最简单的RTP发送H.264裸流开始,用VLC或ffplay接收。能收到画面了,再逐步加入RTCP、分片、时间戳同步。每一步都会踩坑,但踩过了,你就真懂了。
嗯,今天就聊到这儿。代码不在多,在于精。你把这个简易服务器跑通了,后面再学RTMP、HLS,会发现很多概念是相通的。