第 24 课:推流到服务器 —— RTMP 协议
# 第 24 课:推流到服务器 —— RTMP 协议
嘿,我是小方。 上一课我们把编码包写成了硬盘上的文件(录制)。这一课换个终点:把同样的包,实时发到远端的直播服务器,让全世界看到 —— 这就是"直播推流"。 中间隔着一个网络协议:RTMP。这一课我照例先铺背景:直播的数据到底发去哪、RTMP 是什么(以及为什么 Flash 都死了它还活着)、几个绕不开的网络词(TCP/服务器/握手/推流码)、推流和录制的本质区别 —— 然后再进源码,看
rtmp_output怎么连服务器、怎么把一个个包推出去、网络卡了又怎么"丢帧保命"(第 18 课埋的伏笔,这一课兑现)。零网络基础也能跟上。
# 24.1 先看全景:点了"开始直播",数据发去哪?
📍我们在哪:流水线的最后一步——"送出"。录制是送到本地硬盘(L23),推流是送到远端服务器。先看这条路的全貌。
你点"开始直播",数据并不是直接飞到观众屏幕上,中间要过好几道:

你的 OBS(编码好的包)→ 你家宽带的上行 → 平台的接收服务器(ingest)→ 平台转码 + CDN 分发 → 千万观众。
本课只讲第一段:OBS 把包"推"到平台的接收服务器。 后面的转码、全球分发,是平台内部的事,和 OBS 无关。
两个关键概念先记住:
- "推流(push)":是你的 OBS 主动把数据发出去到平台服务器(不是平台来"拉")。
- 平台给你两样东西:一个服务器地址(像
rtmp://live.xxx.com/app)+ 一个推流码(stream key)。地址告诉 OBS"发到哪台服务器",推流码告诉平台"这路流是谁、进哪个直播间"。
而这一路"发出去"用的网络协议,就是本课主角 —— RTMP。
# 24.2 RTMP 是什么?为什么 Flash 死了它还活着?
📍为什么讲这个:上一节说"用 RTMP 发出去"。可 RTMP 是个什么东西、你为什么会在直播设置里到处看到它?这得从它的身世说起。

RTMP 全称 Real-Time Messaging Protocol(实时消息传输协议),是 2002 年 Adobe/Macromedia 为 Flash 设计的。它跑在 TCP 上(下一节讲 TCP),作用是把一路"音视频 + 元数据"实时发给服务器 —— 正是"直播推流"要的。数据在 RTMP 上用 FLV 格式打包(第 22 课那个 Flash 老容器,24.7 细看)。
有意思的是:Flash 在 2020 年正式停用、彻底死了,RTMP 却活了下来。 为什么?
- 它简单、成熟、延迟低,推流够用;
- 全世界的直播平台(Twitch、YouTube、B 站、抖音……)都收 RTMP 推流 —— 形成了事实标准,换成本太高。
于是就出现一个有趣的局面:观众端早就不用 Flash 了(现在都是 HLS/DASH 等现代技术),但**"主播 → 平台"这一入口**,至今仍是 RTMP(或它的加密变体 RTMPS)。所以你学推流,绕不开它。
# 24.3 先补四个网络词:TCP、服务器、握手、推流码
📍为什么现在讲这个:后面源码里会反复出现 TCP、连服务器、握手、推流码。零基础的话,这四个词先讲清,后面就顺了(和第 23 课开头补"进程/管道"一个道理)。

- ① TCP:网上传数据的一种**"可靠水管"** —— 字节按顺序到、丢了会自动重传。RTMP 就跑在 TCP 上。(对比 UDP:快,但不保证到、不保证顺序。)
- ② 服务器:一台在网络上**"等着别人来连"的电脑**。平台的"接收服务器(ingest)"就是它,地址就是那个
rtmp://...URL;你的 OBS 主动去"连"它。 - ③ 握手(handshake):连上服务器后,先来回几轮"对暗号",确认双方能对话、版本/参数一致。RTMP 握手 =
C0/C1/C2 <-> S0/S1/S2几个来回(下面源码里能看到)。 - ④ 推流码(stream key):你的**"频道密码"**。平台靠它认出:这路流是你的、该进你的直播间。⚠️ 千万别泄露 —— 别人拿到就能冒名往你的直播间推流。
把四个词串起来,推流就是这么回事:OBS 通过 TCP 连上平台的服务器(rtmp:// 地址)→ 先握手对上暗号 → 报上推流码表明身份 → 然后把一个个编码包源源不断发过去。
接下来先想清楚一件事:推流和上一课的"录制",到底有什么本质不同?
# 24.4 录制 vs 推流:同样的包,不同的归宿
📍承上启下:L23 录制、本课推流,拿到的是同一批交织好的编码包。差别全在"送到哪、怎么送"—— 而这个差别,决定了后面"网络卡了怎么办"。

| 录制(第 23 课) | 推流(本课) | |
|---|---|---|
| 归宿 | 本地硬盘的一个文件 | 远端的直播服务器 |
| 通道 | 磁盘(几乎总能写进去) | 网络(可能卡、可能丢) |
| 在什么容器里 | MP4 / MKV … | FLV(在 RTMP 上) |
| 写的调用 | av_interleaved_write_frame → 文件 | RTMP_Write → socket |
| 出问题时 | 崩了只影响本地文件 | 卡了必须"丢帧"保命(实时!) |
核心区别:录制"宁等不丢"(硬盘能等你);推流"实时不能等"(观众在等)。 磁盘几乎总能把缓冲写进去,大不了慢一点;但网络一旦跟不上,你不能让观众无限等 —— 只能"丢掉一些帧"来追上进度(24.8 的主题)。
看清了终点和约束,现在进代码,从"连上服务器"开始。
# 24.5 第一步:连上服务器(try_connect)
📍进代码了:
rtmp_output(第 22 课那个带SERVICE标志的输出)。它的.start不直接连,而是开一个"连接线程"(网络连接可能慢,不能卡界面),线程里一步步用 librtmp 连上平台。

rtmp_stream_start(rtmp-stream.c:1397)先 pthread_create 一个 connect_thread;线程里 init_connect(:1231)从绑定的 obs_service 取出服务器地址 + 推流码(obs_service_get_connect_info,:1259),再调 try_connect(:1164)真正连。try_connect 里的 librtmp 调用,按顺序:
// plugins/obs-outputs/rtmp-stream.c,try_connect(抽调用)
RTMP_Init(&stream->rtmp); // 准备连接对象 :1176
RTMP_SetupURL(&stream->rtmp, stream->path.array); // 解析 rtmp:// 地址 :1178
RTMP_EnableWrite(&stream->rtmp); // 声明「我是推流方」 :1181
RTMP_AddStream(&stream->rtmp, stream->key.array); // 带上推流码 :1206
if (!RTMP_Connect(&stream->rtmp, NULL)) // ★ TCP 连接 + RTMP 握手 :1216
return OBS_OUTPUT_CONNECT_FAILED;
if (!RTMP_ConnectStream(&stream->rtmp, 0)) // ★ 发 publish = 开播 :1221
return OBS_OUTPUT_INVALID_STREAM;
return init_send(stream); // 连成功 → 准备发送 :1228
2
3
4
5
6
7
8
9
10
RTMP_Connect(:1216) 是关键:它建立 TCP 连接 + 完成 RTMP 握手。RTMP_ConnectStream(:1221) 发出publish命令,告诉服务器"我要开始推这路流了"。- 全部成功后,
init_send里才调obs_output_begin_data_capture(:1087) —— 连上了,才打开数据阀门,让编码包开始流进来。
🔧 握手不是"发一条请求"那么简单:它是一场多回合的对话(
C0/C1/C2 <-> S0/S1/S2,再加 AMFconnect命令)。(这里的 AMF = Action Message Format,是 RTMP 用来传命令/元数据的消息格式;和第 20 课那个 AMD 显卡编码器 AMF 是两码事,别搞混。)真正的握手代码在 vendored librtmp(plugins/obs-outputs/librtmp/rtmp.c,4 空格缩进的 Flash 时代老代码,HandShake在:3997,RTMP_Connect在:1037,里面还设了TCP_NODELAY降低延迟:925)。OBS 自己不实现 RTMP,而是复用这个成熟老库 —— 自己只管"按顺序调这几个函数"(又是"复用轮子"的思路,和把编码交给 x264、封装交给 libavformat 一脉相承)。
连上了,怎么把包发出去?
# 24.6 第二步:发送 —— 一条队列,两个线程
📍接上一节:阀门打开后,编码包开始涌进来。但"收包"和"发包"的速度不一样(网络忽快忽慢),所以中间夹一条队列缓冲。

这是一个经典的生产者 / 消费者结构:
- 生产者
rtmp_stream_data(:1652):引擎交织好的每个编码包到达这里(它就是obs_output_info里注册的.encoded_packet回调)。它把包入队(deque_push_back进stream->packets,:1412)+os_sem_post唤醒发送线程(:1712)。 - 队列
stream->packets:一个环形缓冲(deque)。网络时快时慢,它削峰填谷。 - 消费者
send_thread(:629):os_sem_wait醒来(:639)→ 出队(:647)→ 调send_packet(:669)。
send_packet(:399)就是"把一个包发到服务器"这一步:
// plugins/obs-outputs/rtmp-stream.c,send_packet(抽骨架)
flv_packet_mux(packet, ..., &data, &size, is_header); // ① 包 → FLV 标签(字节) :408
ret = RTMP_Write(&stream->rtmp, (char *)data, (int)size, 0); // ② 推上 socket :414
stream->total_bytes_sent += size; // 记账 :422
2
3
4
和第 23 课对照,结构一模一样,只是"终点"不同:
- 录制:交织好的包 →
av_interleaved_write_frame→ 写进文件; - 推流:交织好的包 →
flv_packet_mux→RTMP_Write→ 发进 socket。
为什么中间要夹一条队列? 因为编码是"匀速"产出的,网络却"忽快忽慢"。队列把两者解耦:产出的包先堆进队列,发送线程有多快发多快。而"队列积压了多少",正好用来判断网络卡不卡 —— 这就是 24.8 丢帧的依据。
先看看 flv_packet_mux 到底把包包成了什么样。
# 24.7 一个包,怎么包成"FLV 标签"发出去
📍接上一节:
send_packet调的flv_packet_mux,把编码包套上 FLV 的"信封"。这就是 RTMP 上真正传输的东西。

flv_packet_mux(flv-mux.c:372)给一个 encoder_packet 套上三层,组成一个 FLV 标签(tag):
- Tag 头:类型(视频/音频)+ 数据大小 + 时间戳 + 流 ID(
flv-mux.c:316起); - 编解码小头:视频是 5 个字节,含关键帧标志和"是序列头还是数据帧"(
flv-mux.c:333); - 负载(payload):包的字节(编码后的 H.264 / AAC 数据)。
RTMP 就是把一个个 tag 按时间戳顺序推给服务器。
注意视频 tag 里那个关键帧标志(源码 flv-mux.c:333):
// plugins/obs-outputs/flv-mux.c:333(视频 tag 的 5 个额外字节)
s_w8(s, packet->keyframe ? 0x17 : 0x27); // 0x17=关键帧 / 0x27=非关键帧
s_w8(s, is_header ? 0 : 1); // 0=序列头(SPS/PPS)/ 1=普通数据帧
s_wb24(s, ct_offset_ms); // 合成时间偏移(B 帧用)
s_write(s, packet->data, packet->size); // 负载
2
3
4
5
它把"这是不是关键帧"明明白白写进了 FLV 标签 —— 下一节网络卡了要丢帧时,就靠这个信息决定"谁能丢、谁不能丢"。
💡 回连第 22 课"FLV 最挑食":FLV 的标签结构里,codec 的位置是给 H.264/AAC 写死的(源码注释
flv-mux.c:26直说了 "hard-coded to h264 and aac")。所以rtmp_output只认h264 + aac(encoded_video_codecs = "h264;av1"、encoded_audio_codecs = "aac",rtmp-stream.c:1805);HEVC/AV1 得走 "增强 RTMP(Enhanced RTMP)" 扩展格式(flv_packet_ex,flv-mux.c:443)才能塞进去。这就是"FLV 挑食"在代码里的真相。
# 24.8 网络卡了怎么办?丢帧保命(第 18 课的兑现)
📍高潮一节:24.4 说过推流"实时不能等"。现在看它具体怎么"丢帧"——而且丢得很讲究,正是第 18 课 I/P/B 帧知识的兑现。

三步:
① 怎么知道卡了? 看发送队列的**"缓冲时长"** = 最新包的 dts − 最老包的 dts。网络跟不上 → 包排队 → 缓冲时长变长。(和磁盘不同:磁盘几乎总能排空队列,网络卡住时队列会无限涨,所以必须丢。)
② 超过阈值就丢(check_to_drop_frames,:1566):
- 缓冲 > 700ms → 丢 B 帧;
- 缓冲 > 900ms → 连 P 帧一起丢(两道阈值,
:1321/:1719)。
③ 谁先丢?看优先级(drop_frames,:1421)—— 这是核心:
// plugins/obs-outputs/rtmp-stream.c:1440
/* do not drop audio data or video keyframes */
if (packet.type == OBS_ENCODER_AUDIO || packet.drop_priority >= highest_priority)
keep; // 音频、关键帧 → 留
else
drop; // B/P 帧 → 丢
2
3
4
5
6
音频永远不丢、关键帧(I)永远保留、先丢 B 帧、再丢 P 帧。 优先级来自编码包的 drop_priority(第 18 课那个 encoder_packet.drop_priority,obs-encoder.h:136;NAL 优先级 0~3,obs-nal.h:27)。
为什么这么丢?—— 第 18 课的伏笔在这兑现😛/B 帧是"和前面帧的差异",丢一个,后面依赖它的帧就全花屏,直到下一个关键帧重新"锚定"画面。所以丢完还会把 min_priority 抬到关键帧级(:1453):后续 P/B 帧也拒收,直到来了新关键帧才恢复(:1641)——反正它们已经解不出正确画面,收了也没用,不如腾出带宽。而音频丢一点点就是可闻的"咔哒"、且音频码率很小(第 21 课),所以死保音频。
🔧 界面上那个"直播健康度"红黄绿灯,就是这里算出来的
congestion(0~1)= 缓冲时长 ÷ 阈值(:1601),丢帧时锁定 1.0(get_congestion回调,:1780/:1819)。另外:现代 OBS 还提供**"动态码率(DBR)"选项 —— 网络卡了不丢帧,而是主动降低编码码率**(:1608),画质降一点但不丢帧,是更平滑的做法。
# 24.9 推流到服务器:全程回顾

把整条路串起来:
编码器(吐 encoder_packet)→ 交织(引擎按 dts 排,第 22 课)→ rtmp_stream_data 入队 → send_thread 出队 → flv_packet_mux(包→FLV 标签)→ RTMP_Write(推上 socket)→ 平台服务器 → 转码/CDN → 观众。
录制 vs 推流,同一批包,靠 obs_output 的 SERVICE 标志分家(第 22 课):
- 录制:… →
av_interleaved_write_frame→ 文件(会写 trailer/moov;宁等不丢); - 推流:… →
RTMP_Write→ 服务器(会握手;卡了丢帧;实时不能等)。
带走两点:① 推流 = TCP 握手连服务器 + 一个个 FLV 标签 RTMP_Write 出去;② 实时性逼出"丢帧保命"—— 音频/关键帧死保,B/P 帧先丢(第 18 课兑现)。
# 24.10 本课小结
- 直播全景:OBS(编码包)→ 上行带宽 → 平台接收服务器(ingest)→ 转码/CDN → 观众。本课只讲第一段:OBS "推流(push)"到服务器。平台给你服务器地址(rtmp://)+ 推流码(stream key)。
- RTMP:Real-Time Messaging Protocol,2002 年 Adobe 为 Flash 设计,跑在 TCP 上,数据用 FLV 打包。Flash 死了它没死 —— 简单通用、所有平台都收,成了推流的事实标准入口(或加密的 RTMPS)。
- 四个网络词:TCP=可靠有序的字节水管(RTMP 跑在它上面);服务器=等连接的电脑(rtmp:// 地址);握手=连上后来回对暗号(
C0/C1/C2 <-> S0/S1/S2);推流码=你的频道密码(别泄露)。 - 录制 vs 推流:同一批包,归宿不同 —— 硬盘文件(可靠、宁等不丢)vs 直播服务器(网络、实时不能等、卡了必须丢帧);写的调用
av_interleaved_write_framevsRTMP_Write;容器 MP4/MKV vs FLV。 - 连接(
try_connect,:1164):连接线程里RTMP_Init→RTMP_SetupURL(解析 rtmp://)→RTMP_EnableWrite(我是推流方)→RTMP_AddStream(推流码)→**RTMP_Connect(TCP+握手,:1216)→RTMP_ConnectStream(publish 开播,:1221)**→成功后obs_output_begin_data_capture(:1087)。握手实现在 vendored librtmp(rtmp.c,HandShake:3997),OBS 只管按序调用。 - 发送(生产者/消费者):
rtmp_stream_data(:1652,.encoded_packet回调)入队+os_sem_post(:1712)→send_thread(:629)出队 →send_packet(:399)=flv_packet_mux(包→FLV 标签,:408)+RTMP_Write(推 socket,:414)。队列缓冲解耦匀速编码与忽快忽慢的网络。 - FLV 标签:Tag 头(类型/大小/时间戳)+ 编解码小头(关键帧标志 0x17/0x27,
flv-mux.c:333)+ 负载。FLV 只认 H.264/AAC(flv-mux.c:26写死),HEVC/AV1 走增强 RTMP(:443)——回连第 22 课"FLV 挑食"。 - 丢帧保命(第 18 课兑现):缓冲时长(最新−最老 dts)> 阈值(700ms 丢 B / 900ms 连 P,
check_to_drop_frames:1566)→drop_frames(:1421)音频/关键帧死保,B/P 帧先丢(:1440);丢后min_priority抬到关键帧级,拒收后续 P/B 直到新关键帧(:1453/:1641)——因为丢一个 P/B 后面全花屏。congestion=缓冲/阈值喂给界面健康度(:1601/:1780);现代可选 DBR(降码率不丢帧,:1608)。
# 24.11 动手 / 观察(本课作业)
- 找服务器地址和推流码:去你常用的平台(B 站/斗鱼/YouTube 直播后台)找到"推流地址(rtmp://…)"和"推流码",填进 OBS →"设置 → 直播"。体会"地址=连哪台服务器、推流码=你是谁"。
- 看直播健康度:开始直播后,看 OBS 右下角的状态条(帧率/码率/掉帧)。如果家里上行不够,故意把视频码率调很高(如 20000),观察"掉帧"上升 —— 那就是本课的
drop_frames在工作。 - 理解为什么丢 P/B 不丢音频:用自己的话说清 —— 为什么网络卡时先丢 B/P 帧、死保关键帧和音频?丢一个 P 帧为什么会"花屏到下一个关键帧"?
- RTMP vs RTMPS:查一下你的平台推流地址是
rtmp://还是rtmps://,想想区别(提示:S = 加密,防推流码被中间人偷看)。 - 翻源码:打开
plugins/obs-outputs/rtmp-stream.c,找try_connect(:1164)里那串RTMP_*调用;再找send_packet(:399)的flv_packet_mux+RTMP_Write;最后看drop_frames(:1440)那句"do not drop audio data or video keyframes"。
# 下一课预告
这一课里,OBS 从一个 obs_service 拿到了"服务器地址 + 推流码"。可这个 obs_service 是怎么来的?为什么你在 OBS 里选"B 站"或"Twitch",它就知道该往哪个服务器推、该用什么参数、甚至能一键授权登录?
第 25 课:直播平台是怎么接入的 —— obs_service —— 我们看 OBS 用 obs_service 这个抽象(又是一张登记表!)统一管理成百上千个直播平台:服务器列表、推荐码率、编码器限制怎么来的(rtmp-services 插件 + 一个 JSON 服务列表),以及"一键授权"背后的 OAuth。模块 6 的收官课。下节课见。
📁 本课配图:
imgs/24-01~imgs/24-09📌 源码锚点:plugins/obs-outputs/rtmp-stream.c::1796(rtmp_output_info,SERVICE)、:1805(encoded_video/audio_codecs h264;av1 / aac)、:1811-1819(callbacks + get_congestion);rtmp-stream.h:55(struct rtmp_stream)、:58-60(packets/sent_headers)、:65-71(connect/send thread)、:80-81(path/key)、:119(RTMP rtmp); 连接::1397(rtmp_stream_start→connect_thread)、:1231(init_connect)、:1247/:1259(obs_output_get_service / obs_service_get_connect_info 取 URL+key)、:1164(try_connect)、:1176(RTMP_Init)、:1178(RTMP_SetupURL)、:1181(RTMP_EnableWrite)、:1206(RTMP_AddStream)、:1216(RTMP_Connect)、:1221(RTMP_ConnectStream)、:1081/:1087(send_meta_data / begin_data_capture)、:928(send_headers)、:805(obs_parse_avc_header); 发送::1652(rtmp_stream_data 入队回调)、:1412(deque_push_back)、:1712(os_sem_post)、:629(send_thread)、:639(os_sem_wait)、:399(send_packet)、:408(flv_packet_mux)、:414(RTMP_Write)、:720(RTMP_Close); 丢帧::1566(check_to_drop_frames)、:1598(缓冲时长)、:1601(congestion)、:1628(阈值比较)、:1321/:1719(阈值 700/900ms)、:1421(drop_frames)、:1440(音频/关键帧保、B/P 丢)、:1453/:1641(min_priority 拒收)、:1608(DBR)、:1780(get_congestion);libobs/obs-encoder.h:128/:136(priority/drop_priority)、libobs/obs-nal.h:27(NAL 优先级); FLV:plugins/obs-outputs/flv-mux.c:372(flv_packet_mux)、:308/:316/:327/:333(视频 tag + 关键帧标志)、:341/:365(音频 tag)、:26(hard-coded h264/aac 注释)、:443(增强 RTMP); librtmp(vendored,Flash 时代老库):plugins/obs-outputs/librtmp/rtmp.c:1037(RTMP_Connect)、:876/:925(TCP + TCP_NODELAY)、:1019/:3997(HandShake)、:1180(RTMP_ConnectStream)、:5318(RTMP_Write)。 注:RTMP/TCP/Flash 的历史与网络概念为通用背景知识,非 OBS 源码内容。
本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。
社区交流
讨论与留言