文档目录

一、RTMP 握手

客户端                            服务器
  │                                │
  │──── C0 (版本=3) ──────────────→│
  │──── C1 (1536 字节随机数据) ────→│
  │←─── S0 (版本=3) ──────────────│
  │←─── S1 (1536 字节随机数据) ────│
  │←─── S2 (C1 的回应) ───────────│
  │──── C2 (S1 的回应) ───────────→│
  │                                │
  │       握手完成,建立连接         │
  │                                │
  │──── connect("live") ──────────→│
  │←─── _result (成功) ───────────│
  │──── createStream ─────────────→│
  │←─── _result (stream_id=1) ────│
  │──── publish("live") ──────────→│
  │←─── onStatus("正在发布") ──────│
  │                                │
  │       开始推送音视频数据         │
  │──── Audio/Video Tag ──────────→│
  │──── Audio/Video Tag ──────────→│
  │ ...                            │

二、RTMP Chunk 分片

RTMP 的核心设计:把大的消息切成小的 chunk,交错发送,避免大消息阻塞后面的小消息。

消息(Message):
  ├── Header (1-18 字节) │ 消息体(可变大小,最大可达 128MB)

消息被切成 Chunk(每个 128 字节):
  Chunk 1: [基本头][消息头][数据 128 字节]
  Chunk 2: [基本头        ][数据 128 字节]  ← 后面的 chunk 可以省略消息头
  Chunk 3: [基本头        ][数据 剩余字节]

为什么需要分片?

假设你同时推送:
  视频包:500KB 的 H.264 关键帧
  音频包:200 字节的 AAC 帧

如果不分片:
  [完整视频包 500KB] → 发完才轮到 [音频包 200 字节]
  客户端听到 500KB 的传输时间里没有声音 → 卡顿

分片后:
  [视频 ch1][视频 ch2][音频 ch1][视频 ch3][视频 ch4][音频 ch2]...
  音频每 128 字节穿插在视频中 → 客户端持续收到声音

三、RTMP URL 的结构

rtmp://server-address:port/app/streamName

例子:
rtmp://live.twitch.tv/app/live_123456
  ↑协议   ↑服务器       ↑应用 ↑流名称

当你推流:
  push("rtmp://server/live/stream1")
  服务器收到:publish "stream1" in app "live"

当你拉流:
  play("rtmp://server/live/stream1")
  服务器找到:app "live" 中名为 "stream1" 的流,转发给你

四、不用第三方库的 RTMP 握手(极简版)

真实的 RTMP 握手复杂(包含 digest),这里展示握手的第一步——验证版本号:

#include <cstdint>
#include <cstring>
#include <sys/socket.h>
#include <netdb.h>

// 极简 RTMP 握手(只做连接建立 + 版本交换)
// 真实的 RTMP 还需要做 digest 验证,这里只展示流程骨架
bool rtmp_handshake(int sockfd) {
    // C0: 客户端声明 RTMP 版本(目前版本 = 3)
    uint8_t c0 = 3;
    if (send(sockfd, &c0, 1, 0) != 1) return false;

    // C1: 1536 字节随机数据(实际包含 time + version + random)
    uint8_t c1[1536] = {};
    uint32_t ts = htonl(0);  // 时间戳
    memcpy(c1, &ts, 4);      // 前 4 字节 = 时间戳
    memcpy(c1 + 4, &ts, 4);  // 接着 4 字节 = 版本(0 = 握手类型)
    // 剩余 1528 字节随机填充
    for (int i = 8; i < 1536; i++) c1[i] = rand() & 0xFF;

    if (send(sockfd, c1, 1536, 0) != 1536) return false;

    // 读取 S0: 服务器版本
    uint8_t s0;
    if (recv(sockfd, &s0, 1, 0) != 1) return false;
    if (s0 != 3) return false;  // 版本不匹配

    // 读取 S1 + S2
    uint8_t buf[3072];
    if (recv(sockfd, buf, 3072, MSG_WAITALL) != 3072) return false;

    // C2: 回显 S1 的数据
    if (send(sockfd, buf, 1536, 0) != 1536) return false;

    return true;  // 握手完成
}