FFmpeg RTSP拉流流程深度解析

FFmpeg RTSP拉流流程深度解析

本文基于FFmpeg 8.0源码,深入剖析RTSP协议拉流的完整实现机制,包含详细的流程图、状态机分析和核心代码解读。


📚 目录

  1. RTSP协议基础
  2. [FFmpeg RTSP架构总览](#FFmpeg RTSP架构总览)
  3. RTSP拉流完整流程
  4. 核心数据结构详解
  5. 状态机设计与转换
  6. 传输模式深度对比
  7. 关键函数源码解析
  8. RTSP服务端监听模式
  9. 错误处理与容错机制
  10. 性能优化与实践建议

1. RTSP协议基础

1.1 什么是RTSP

RTSP(Real Time Streaming Protocol,实时流传输协议)是一种应用层协议,用于控制流媒体服务器的实时传输。它并不直接传输音视频数据,而是作为"遥控器"来控制媒体流的播放、暂停、定位等操作。

1.2 RTSP核心特性

特性 说明
协议类型 应用层协议,基于文本
默认端口 554(RTSP)、322(RTSPS加密)
传输协议 默认使用TCP作为控制信道
数据传输 通常使用RTP/RTCP协议
会话管理 基于Session ID的会话机制
状态管理 有状态协议,维护客户端-服务器状态

1.3 RTSP与RTP的关系

复制代码
┌─────────────┐    控制命令          ┌─────────────┐
│ RTSP客户端  │ ───────────────────> │ RTSP服务器  │
│             │ OPTIONS/DESCRIBE    │             │
│             │ SETUP/PLAY          │             │
└──────┬──────┘                     └──────┬──────┘
       │                                    │
       │ Session管理                        │
       │ <─────────────────────────────────│
       │                                    │
       │                                    │ 媒体流
       │                                    ▼
       │                            ┌─────────────┐
       │                            │  RTP/RTCP   │
       │                            └──────┬──────┘
       │                                   │
       │                                   │ UDP或TCP
       │                                   ▼
       │                            ┌─────────────┐
       │                            │ 媒体数据接收 │
       │                            └─────────────┘
       │
       └────────────────────────────────────┘

核心区别

  • RTSP: 控制层,发送命令(类似HTTP)
  • RTP: 传输层,传输音视频数据包
  • RTCP: 控制协议,提供QoS反馈

1.4 RTSP核心命令

命令 功能 说明
OPTIONS 查询功能 询问服务器支持哪些方法
DESCRIBE 获取描述 获取媒体流的SDP描述信息
SETUP 建立会话 为单个流建立传输机制(UDP/TCP)
PLAY 开始播放 启动数据传输
PAUSE 暂停播放 暂停数据传输(保持会话)
TEARDOWN 终止会话 释放所有资源,结束会话
ANNOUNCE 公告(服务端模式) 客户端向服务器发送媒体描述
RECORD 录制(服务端模式) 开始接收数据

2. FFmpeg RTSP架构总览

2.1 核心文件组成

FFmpeg的RTSP实现主要由以下文件组成:

复制代码
libavformat/
├── rtsp.h          # RTSP核心数据结构和函数声明
├── rtsp.c          # RTSP协议处理核心逻辑(2700+行)
├── rtspdec.c       # RTSP解复用器(拉流)(1011行)
├── rtspenc.c       # RTSP复用器(推流)(260行)
├── rtpproto.c      # RTP协议实现
├── rtpdec.c        # RTP解复用
└── sdp.c           # SDP解析

2.2 模块交互关系

复制代码
┌─────────────────────────────────────────┐
│           应用层                        │
│  ┌───────────────────────────────────┐  │
│  │  FFmpeg应用 (ffplay/ffmpeg)      │  │
│  └──────────────┬────────────────────┘  │
└─────────────────┼──────────────────────┘
                  │
┌─────────────────▼──────────────────────┐
│          AVFormat层                     │
│  ┌──────────────────┐  ┌─────────────┐ │
│  │ RTSP Demuxer     │  │ RTSP Core   │ │
│  │ (rtspdec.c)      │  │ (rtsp.c)    │ │
│  └────────┬─────────┘  └──────┬──────┘ │
└───────────┼────────────────────┼───────┘
            │                    │
            │                    │
┌───────────▼────────────────────▼───────┐
│           传输层                        │
│  ┌──────────────┐  ┌────────────────┐ │
│  │ RTP Protocol │  │ TCP Connection  │ │
│  │ (rtpproto.c) │  │ UDP Socket      │ │
│  └──────┬───────┘  └────────────────┘ │
└─────────┼──────────────────────────────┘
          │
┌─────────▼──────────────────────────────┐
│           解析层                        │
│  ┌──────────────┐  ┌────────────────┐ │
│  │ SDP Parser   │  │ RTP Decoder    │ │
│  │ (sdp.c)      │  │ (rtpdec.c)     │ │
│  └──────────────┘  └────────────────┘ │
└─────────────────────────────────────────┘

2.3 架构分层

层次 职责 核心组件
应用层 用户接口调用 ffplay, ffmpeg, API调用
Demuxer层 流解复用控制 rtsp_read_header(), rtsp_read_packet()
协议层 RTSP协议处理 ff_rtsp_send_cmd(), ff_rtsp_connect()
传输层 数据收发 RTP over UDP/TCP
解析层 数据包解析 SDP解析, RTP解包

3. RTSP拉流完整流程

3.1 整体流程图

RTSP拉流完整时序流程

复制代码
┌─────────────┐                                    ┌─────────────┐
│ RTSP客户端  │                                    │ RTSP服务器  │
└──────┬──────┘                                    └──────┬──────┘
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段1: 初始化连接                           │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──TCP连接(端口554)───────────────────────────────>│
       │<───────────────────────────────连接建立──────────│
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段2: 能力协商                             │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──OPTIONS rtsp://server/stream───────────────────>│
       │<──200 OK                                         │
       │    Public: DESCRIBE,SETUP,PLAY...                │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段3: 获取媒体描述                         │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──DESCRIBE rtsp://server/stream───────────────────>│
       │    Accept: application/sdp                        │
       │<──200 OK                                         │
       │    Content-Type: application/sdp                 │
       │    [SDP内容]                                     │
       │                                                   │
       │  [解析SDP,创建媒体流]                            │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段4: 建立传输通道(每个流)               │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──SETUP rtsp://server/stream/track1──────────────>│
       │    Transport: RTP/AVP;unicast;client_port=5000-5001│
       │<──200 OK                                         │
       │    Session: 12345678                             │
       │    Transport: RTP/AVP;server_port=6000-6001      │
       │                                                   │
       │──SETUP rtsp://server/stream/track2──────────────>│
       │    Transport: RTP/AVP;unicast;client_port=5002-5003│
       │<──200 OK                                         │
       │    Session: 12345678                             │
       │    Transport: RTP/AVP;server_port=6002-6003      │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段5: 开始播放                           │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──PLAY rtsp://server/stream───────────────────────>│
       │    Session: 12345678                             │
       │    Range: npt=0.000-                             │
       │<──200 OK                                         │
       │    RTP-Info: url=track1;seq=xxx;rtptime=yyy      │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段6: 数据传输(持续)                     │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │<──RTP数据包(音视频)────────────────────────────│
       │──RTCP反馈(周期性)─────────────────────────────>│
       │<──RTP数据包(音视频)────────────────────────────│
       │──RTCP反馈(周期性)─────────────────────────────>│
       │    ... (持续传输)                                │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段7: 保活机制(周期性)                   │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──OPTIONS/GET_PARAMETER──────────────────────────>│
       │    Session: 12345678                             │
       │<──200 OK                                         │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段8: 结束会话                             │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──TEARDOWN rtsp://server/stream──────────────────>│
       │    Session: 12345678                             │
       │<──200 OK                                         │
       │                                                   │
       │  [关闭所有连接]                                  │
       │                                                   │

时序表格式

阶段 客户端操作 服务器响应 说明
1. 初始化 TCP连接(端口554) 连接建立 建立控制通道
2. 能力协商 OPTIONS请求 200 OK Public: DESCRIBE,SETUP,PLAY... 查询支持的方法
3. 获取描述 DESCRIBE请求 Accept: application/sdp 200 OK Content-Type: application/sdp [SDP内容] 获取媒体流描述
4. 建立传输 SETUP track1 Transport: RTP/AVP;unicast;client_port=5000-5001 200 OK Session: 12345678 Transport: ...;server_port=6000-6001 为每个流建立传输通道
SETUP track2 Transport: RTP/AVP;unicast;client_port=5002-5003 200 OK Session: 12345678 Transport: ...;server_port=6002-6003
5. 开始播放 PLAY请求 Session: 12345678 Range: npt=0.000- 200 OK RTP-Info: url=track1;seq=xxx;rtptime=yyy 启动数据传输
6. 数据传输 接收RTP包 发送RTCP反馈 发送RTP包 接收RTCP反馈 持续传输音视频数据
7. 保活 OPTIONS/GET_PARAMETER Session: 12345678 200 OK 防止连接超时
8. 结束 TEARDOWN请求 Session: 12345678 200 OK 释放所有资源

3.2 详细阶段分解

阶段1: 初始化(rtsp_read_header)
c 复制代码
// 文件: rtspdec.c
static int rtsp_read_header(AVFormatContext *s)
{
    RTSPState *rt = s->priv_data;
    int ret;
    
    // 判断是客户端模式还是服务端监听模式
    if (rt->rtsp_flags & RTSP_FLAG_LISTEN) {
        ret = rtsp_listen(s);  // 服务端模式
    } else {
        ret = ff_rtsp_connect(s);  // 客户端模式
        // 如果不是初始暂停,立即开始播放
        if (!rt->initial_pause) {
            ret = rtsp_read_play(s);
        }
    }
    return ret;
}

流程说明

  1. 检查网络环境初始化
  2. 解析URL提取主机、端口、路径
  3. 建立TCP控制连接(默认端口554)
  4. 初始化状态机为IDLE状态
阶段2: DESCRIBE获取SDP
c 复制代码
// 文件: rtsp.c (简化逻辑)
int ff_rtsp_setup_input_streams(AVFormatContext *s, RTSPMessageHeader *reply)
{
    RTSPState *rt = s->priv_data;
    char cmd[MAX_URL_SIZE];
    unsigned char *content = NULL;
    
    // 构造DESCRIBE请求
    snprintf(cmd, sizeof(cmd), "Accept: application/sdp\r\n");
    
    // 发送DESCRIBE命令
    ff_rtsp_send_cmd(s, "DESCRIBE", rt->control_uri, cmd, reply, &content);
    
    if (reply->status_code != RTSP_STATUS_OK) {
        return ff_rtsp_averror(reply->status_code, AVERROR_INVALIDDATA);
    }
    
    // 解析SDP描述
    ret = ff_sdp_parse(s, (const char *)content);
    av_freep(&content);
    
    return ret;
}

SDP示例

sdp 复制代码
v=0
o=- 1234567890 1234567890 IN IP4 192.168.1.100
s=RTSP Stream
c=IN IP4 0.0.0.0
t=0 0
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
a=control:track1
m=audio 0 RTP/AVP 97
a=rtpmap:97 MPEG4-GENERIC/44100/2
a=control:track2
阶段3: SETUP建立传输
c 复制代码
// 简化的SETUP逻辑
int ff_rtsp_make_setup_request(AVFormatContext *s, const char *host, int port,
                                int lower_transport, const char *real_challenge)
{
    RTSPState *rt = s->priv_data;
    RTSPMessageHeader reply;
    
    for (int i = 0; i < rt->nb_rtsp_streams; i++) {
        RTSPStream *rtsp_st = rt->rtsp_streams[i];
        char transport[2048];
        
        // 构造Transport头
        if (lower_transport == RTSP_LOWER_TRANSPORT_TCP) {
            snprintf(transport, sizeof(transport),
                     "Transport: RTP/AVP/TCP;unicast;interleaved=%d-%d",
                     i * 2, i * 2 + 1);
        } else {
            snprintf(transport, sizeof(transport),
                     "Transport: RTP/AVP;unicast;client_port=%d-%d",
                     rt->rtp_port_min + i * 2, rt->rtp_port_min + i * 2 + 1);
        }
        
        // 发送SETUP命令
        ff_rtsp_send_cmd(s, "SETUP", rtsp_st->control_url, 
                        transport, &reply, NULL);
        
        // 保存Session ID(第一次)
        if (i == 0)
            av_strlcpy(rt->session_id, reply.session_id, sizeof(rt->session_id));
    }
    
    return 0;
}
阶段4: PLAY开始播放
c 复制代码
// 文件: rtspdec.c
static int rtsp_read_play(AVFormatContext *s)
{
    RTSPState *rt = s->priv_data;
    RTSPMessageHeader reply1, *reply = &reply1;
    char cmd[MAX_URL_SIZE];
    
    // 发送NAT穿透包(UDP模式)
    if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP) {
        for (int i = 0; i < rt->nb_rtsp_streams; i++) {
            ff_rtp_send_punch_packets(rt->rtsp_streams[i]->rtp_handle);
        }
    }
    
    // 构造Range参数(用于seek)
    if (rt->state != RTSP_STATE_PAUSED) {
        snprintf(cmd, sizeof(cmd),
                 "Range: npt=%"PRId64".%03"PRId64"-\r\n",
                 rt->seek_timestamp / AV_TIME_BASE,
                 rt->seek_timestamp / (AV_TIME_BASE / 1000) % 1000);
    } else {
        cmd[0] = 0;
    }
    
    // 发送PLAY命令
    ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
    
    if (reply->status_code != RTSP_STATUS_OK) {
        return ff_rtsp_averror(reply->status_code, -1);
    }
    
    // 更新状态为STREAMING
    rt->state = RTSP_STATE_STREAMING;
    return 0;
}
阶段5: 数据接收循环
c 复制代码
// 文件: rtspdec.c
static int rtsp_read_packet(AVFormatContext *s, AVPacket *pkt)
{
    RTSPState *rt = s->priv_data;
    int ret;
    
retry:
    // 获取RTP数据包
    ret = ff_rtsp_fetch_packet(s, pkt);
    
    if (ret < 0) {
        // UDP超时,尝试切换到TCP
        if (ret == AVERROR(ETIMEDOUT) && !rt->packets) {
            if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP &&
                rt->lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) {
                av_log(s, AV_LOG_WARNING, "UDP timeout, retrying with TCP\n");
                
                rtsp_read_pause(s);
                rt->session_id[0] = '\0';
                
                if (resetup_tcp(s) == 0) {
                    rt->state = RTSP_STATE_IDLE;
                    rtsp_read_play(s);
                    goto retry;
                }
            }
        }
        return ret;
    }
    
    rt->packets++;
    
    // 发送保活命令(超时时间的一半)
    if ((av_gettime_relative() - rt->last_cmd_time) / 1000000 >= rt->timeout / 2) {
        if (rt->server_type == RTSP_SERVER_WMS || rt->get_parameter_supported) {
            ff_rtsp_send_cmd_async(s, "GET_PARAMETER", rt->control_uri, NULL);
        } else {
            ff_rtsp_send_cmd_async(s, "OPTIONS", rt->control_uri, NULL);
        }
    }
    
    return 0;
}

3.3 命令交互时序表

序号 命令 方向 触发时机 关键参数
1 OPTIONS C→S 连接建立后 -
2 DESCRIBE C→S OPTIONS之后 Accept: application/sdp
3 SETUP C→S 每个媒体流 Transport, Session
4 PLAY C→S 所有流SETUP完成 Range, Session
5 GET_PARAMETER/OPTIONS C→S 周期性保活 Session
6 PAUSE C→S 用户暂停 Session
7 TEARDOWN C→S 结束播放 Session

4. 核心数据结构详解

4.1 RTSPState - 全局状态管理

c 复制代码
typedef struct RTSPState {
    const AVClass *class;
    
    // === 连接管理 ===
    URLContext *rtsp_hd;          // RTSP TCP控制连接句柄
    URLContext *rtsp_hd_out;      // 输出连接(HTTP隧道)
    
    // === 流管理 ===
    int nb_rtsp_streams;          // 流的数量
    struct RTSPStream **rtsp_streams; // 流数组
    
    // === 状态管理 ===
    enum RTSPClientState state;   // 当前状态(IDLE/PAUSED/STREAMING)
    int seq;                      // RTSP命令序列号
    char session_id[512];         // 会话ID
    int64_t seek_timestamp;       // Seek目标时间戳
    
    // === 传输配置 ===
    enum RTSPTransport transport;           // RTP/RDT
    enum RTSPLowerTransport lower_transport; // UDP/TCP
    enum RTSPServerType server_type;        // 服务器类型
    
    // === 保活机制 ===
    int timeout;                  // 服务器超时时间(秒)
    int64_t last_cmd_time;        // 上次命令时间
    
    // === 端口配置 ===
    int rtp_port_min;             // RTP最小端口(默认5000)
    int rtp_port_max;             // RTP最大端口(默认65000)
    
    // === 认证 ===
    char auth[128];               // 认证信息
    HTTPAuthState auth_state;     // 认证状态
    
    // === 缓冲管理 ===
    uint8_t *recvbuf;             // 接收缓冲区
    int buffer_size;              // 缓冲区大小
    int reordering_queue_size;    // 重排序队列大小
    
    // === 统计信息 ===
    uint64_t packets;             // 收到的包数量
    int nb_byes;                  // 收到的BYE包数量
    
} RTSPState;

关键字段说明

字段 类型 作用
state enum 状态机核心,控制命令流程
session_id char[512] 会话唯一标识,每个命令必带
seq int CSeq序列号,确保命令顺序
lower_transport enum UDP/TCP选择,影响数据传输方式
rtsp_streams 指针数组 管理所有媒体流

4.2 RTSPStream - 单个媒体流

c 复制代码
typedef struct RTSPStream {
    // === 连接 ===
    URLContext *rtp_handle;       // RTP连接(UDP模式)
    void *transport_priv;         // RTP解析上下文
    
    // === 流信息 ===
    int stream_index;             // 对应的AVStream索引
    char control_url[MAX_URL_SIZE]; // 流的控制URL
    
    // === TCP交错模式 ===
    int interleaved_min;          // 交错通道最小ID
    int interleaved_max;          // 交错通道最大ID
    
    // === SDP信息 ===
    int sdp_port;                 // SDP中的端口
    struct sockaddr_storage sdp_ip; // SDP中的IP地址
    int sdp_payload_type;         // 负载类型
    
    // === 动态协议 ===
    const RTPDynamicProtocolHandler *dynamic_handler;
    PayloadContext *dynamic_protocol_context;
    
    // === RTCP ===
    int feedback;                 // 是否启用RTCP反馈
    uint32_t ssrc;                // 同步源标识符
    
} RTSPStream;

4.3 RTSPMessageHeader - 消息头

c 复制代码
typedef struct RTSPMessageHeader {
    // === 基础信息 ===
    int content_length;           // 内容长度
    enum RTSPStatusCode status_code; // 状态码(200, 404等)
    int seq;                      // CSeq序列号
    
    // === 会话管理 ===
    char session_id[512];         // 会话ID
    int timeout;                  // 超时时间
    
    // === 传输配置 ===
    int nb_transports;            // Transport数量
    RTSPTransportField transports[RTSP_MAX_TRANSPORTS];
    
    // === 播放控制 ===
    int64_t range_start;          // Range开始时间
    int64_t range_end;            // Range结束时间
    
    // === 服务器信息 ===
    char server[64];              // Server字段
    char content_type[64];        // Content-Type
    char reason[256];             // 状态原因
    
    // === Real特殊字段 ===
    char real_challenge[64];      // RealChallenge
    
} RTSPMessageHeader;

4.4 RTSPTransportField - 传输字段

c 复制代码
typedef struct RTSPTransportField {
    // === 端口配置 ===
    int client_port_min;          // 客户端RTP端口
    int client_port_max;          // 客户端RTCP端口
    int server_port_min;          // 服务器RTP端口
    int server_port_max;          // 服务器RTCP端口
    int port_min, port_max;       // 多播端口
    
    // === TCP交错 ===
    int interleaved_min;          // 交错最小通道
    int interleaved_max;          // 交错最大通道
    
    // === 传输协议 ===
    enum RTSPTransport transport; // RTP/RDT
    enum RTSPLowerTransport lower_transport; // UDP/TCP
    
    // === 网络配置 ===
    struct sockaddr_storage destination; // 目标地址
    char source[INET6_ADDRSTRLEN + 1];  // 源地址
    int ttl;                      // 多播TTL
    int mode_record;              // 录制模式标志
    
} RTSPTransportField;

5. 状态机设计与转换

5.1 状态定义

c 复制代码
enum RTSPClientState {
    RTSP_STATE_IDLE,      // 空闲:未初始化或已TEARDOWN
    RTSP_STATE_STREAMING, // 流媒体中:正在接收数据
    RTSP_STATE_PAUSED,    // 暂停:已SETUP但未PLAY
    RTSP_STATE_SEEKING,   // Seeking:请求跳转中
};

5.2 状态转换图

RTSP状态机转换流程

复制代码
                    ┌─────────┐
                    │  初始化  │
                    └────┬────┘
                         │
                         ▼
                    ┌─────────┐
                    │  IDLE   │◄──────────┐
                    │ 空闲状态 │           │
                    └────┬────┘           │
                         │                │
         OPTIONS/DESCRIBE│                │
                         │                │
                         │ SETUP成功      │
                         ▼                │
                    ┌─────────┐           │
                    │ PAUSED  │           │
                    │ 暂停状态 │           │
                    └────┬────┘           │
                         │                │
         SETUP更多流     │                │
                         │                │
                         │ PLAY成功       │
                         ▼                │
                    ┌─────────┐           │
                    │STREAMING│           │
                    │流媒体状态│           │
                    └────┬────┘           │
                         │                │
         av_seek_frame() │                │
                         │                │
                         ▼                │
                    ┌─────────┐           │
                    │ SEEKING │           │
                    │ 跳转状态 │           │
                    └────┬────┘           │
                         │                │
                         │ PLAY成功       │
                         │                │
                         └────────────────┘
                         
         PAUSE ──────────┘
         TEARDOWN ───────┘
         TEARDOWN ───────┘
         TEARDOWN ───────┘

状态说明表

状态 可执行命令 说明
IDLE OPTIONS DESCRIBE TEARDOWN 空闲状态,未建立会话
PAUSED SETUP PLAY OPTIONS TEARDOWN 已建立传输通道,但未开始播放
STREAMING PAUSE OPTIONS GET_PARAMETER TEARDOWN 正在接收数据流
SEEKING - 临时状态,执行跳转操作

5.3 状态转换条件表

当前状态 触发事件 目标状态 执行的RTSP命令 代码位置
IDLE 连接成功 + SETUP PAUSED SETUP ff_rtsp_make_setup_request()
PAUSED 开始播放 STREAMING PLAY rtsp_read_play()
STREAMING 用户暂停 PAUSED PAUSE rtsp_read_pause()
STREAMING 用户Seek SEEKING PAUSE + PLAY rtsp_read_seek()
SEEKING Seek完成 STREAMING - rtsp_read_play()
任意 关闭连接 IDLE TEARDOWN rtsp_read_close()

5.4 状态校验机制

在服务端监听模式下,FFmpeg会严格校验命令是否符合当前状态:

c 复制代码
// 文件: rtspdec.c - parse_command_line()
if (rt->state == RTSP_STATE_IDLE) {
    // IDLE状态只接受ANNOUNCE和OPTIONS
    if ((*methodcode != ANNOUNCE) && (*methodcode != OPTIONS)) {
        av_log(s, AV_LOG_ERROR, "Unexpected command in Idle State %s\n", line);
        return AVERROR_PROTOCOL_NOT_FOUND;
    }
} else if (rt->state == RTSP_STATE_PAUSED) {
    // PAUSED状态接受OPTIONS、RECORD、SETUP
    if ((*methodcode != OPTIONS) && (*methodcode != RECORD) && (*methodcode != SETUP)) {
        av_log(s, AV_LOG_ERROR, "Unexpected command in Paused State %s\n", line);
        return AVERROR_PROTOCOL_NOT_FOUND;
    }
} else if (rt->state == RTSP_STATE_STREAMING) {
    // STREAMING状态接受PAUSE、OPTIONS、TEARDOWN
    if ((*methodcode != PAUSE) && (*methodcode != OPTIONS) && (*methodcode != TEARDOWN)) {
        av_log(s, AV_LOG_ERROR, "Unexpected command in Streaming State %s\n", line);
        return AVERROR_PROTOCOL_NOT_FOUND;
    }
}

6. 传输模式深度对比

6.1 传输模式概览

FFmpeg RTSP支持三种底层传输模式:

c 复制代码
enum RTSPLowerTransport {
    RTSP_LOWER_TRANSPORT_UDP = 0,           // UDP单播
    RTSP_LOWER_TRANSPORT_TCP = 1,           // TCP交错
    RTSP_LOWER_TRANSPORT_UDP_MULTICAST = 2, // UDP组播
};

6.2 详细对比表

特性 UDP单播 TCP交错 UDP组播
传输协议 UDP TCP UDP
端口使用 每个流2个端口 (RTP+RTCP) 复用RTSP连接 (通道ID区分) 组播地址+端口
NAT穿透 需要打洞 天然支持 不支持
带宽效率 ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
可靠性 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐
延迟
适用场景 局域网,对丢包不敏感 公网,需要可靠传输 多客户端同播
防火墙友好 ⭐⭐ ⭐⭐⭐⭐⭐

6.3 UDP单播模式

工作原理

UDP单播模式架构

复制代码
┌─────────────────────────────────────────────────────┐
│                    客户端                            │
│  ┌────────────────┐  ┌──────────────────────────┐   │
│  │ RTSP控制      │  │  视频流                  │   │
│  │ TCP 554       │  │  RTP接收 UDP 5000        │   │
│  │               │  │  RTCP接收 UDP 5001      │   │
│  └───────┬───────┘  └──────────────────────────┘   │
│          │                                          │
│          │         ┌──────────────────────────┐   │
│          │         │  音频流                  │   │
│          │         │  RTP接收 UDP 5002        │   │
│          │         │  RTCP接收 UDP 5003      │   │
│          │         └──────────────────────────┘   │
└──────────┼──────────────────────────────────────────┘
           │
           │ 控制命令 (双向)
           │
┌──────────▼──────────────────────────────────────────┐
│                    服务器                            │
│  ┌────────────────┐  ┌──────────────────────────┐   │
│  │ RTSP控制      │  │  视频流                  │   │
│  │ TCP 554       │  │  RTP发送 UDP 6000        │   │
│  │               │  │  RTCP发送 UDP 6001       │   │
│  └───────────────┘  └──────────────────────────┘   │
│                                                    │
│         ┌──────────────────────────┐              │
│         │  音频流                  │              │
│         │  RTP发送 UDP 6002        │              │
│         │  RTCP发送 UDP 6003       │              │
│         └──────────────────────────┘              │
└────────────────────────────────────────────────────┘

数据流向:
  RTP: 服务器 → 客户端 (单向)
  RTCP: 客户端 ↔ 服务器 (双向反馈)
SETUP请求示例
http 复制代码
SETUP rtsp://server/stream/track1 RTSP/1.0
CSeq: 3
Transport: RTP/AVP;unicast;client_port=5000-5001
User-Agent: FFmpeg
服务器响应
http 复制代码
RTSP/1.0 200 OK
CSeq: 3
Session: 12345678;timeout=60
Transport: RTP/AVP;unicast;client_port=5000-5001;server_port=6000-6001
核心代码
c 复制代码
// UDP模式的SETUP处理
if (request.transports[0].lower_transport == RTSP_LOWER_TRANSPORT_UDP) {
    int localport = rt->rtp_port_min;
    
    do {
        AVDictionary *opts = NULL;
        av_dict_set_int(&opts, "buffer_size", rt->buffer_size, 0);
        
        // 打开UDP端口
        ff_url_join(url, sizeof(url), "rtp", NULL, host, localport, NULL);
        ret = ffurl_open_whitelist(&rtsp_st->rtp_handle, url, 
                                   AVIO_FLAG_READ_WRITE,
                                   &s->interrupt_callback, &opts,
                                   s->protocol_whitelist, 
                                   s->protocol_blacklist, NULL);
        av_dict_free(&opts);
        
        if (ret)
            localport += 2;  // RTP端口必须是偶数
    } while (ret || localport > rt->rtp_port_max);
    
    // 获取实际绑定的本地端口
    localport = ff_rtp_get_local_rtp_port(rtsp_st->rtp_handle);
    
    // 构造响应
    snprintf(responseheaders, sizeof(responseheaders), 
             "Transport: RTP/AVP/UDP;unicast;mode=record;"
             "client_port=%d-%d;server_port=%d-%d\r\n",
             request.transports[0].client_port_min,
             request.transports[0].client_port_max, 
             localport, localport + 1);
}

6.4 TCP交错模式

工作原理

TCP交错模式架构

复制代码
┌─────────────────────────────────────────────────────┐
│                    客户端                            │
│  ┌──────────────────────────────────────────────┐   │
│  │      RTSP/RTP 统一TCP连接 (端口554)          │   │
│  │                                               │   │
│  │  ┌──────────────────────────────────────┐   │   │
│  │  │  TCP连接内部通道复用                 │   │   │
│  │  │                                      │   │   │
│  │  │  通道0: Video RTP                   │   │   │
│  │  │  通道1: Video RTCP                  │   │   │
│  │  │  通道2: Audio RTP                   │   │   │
│  │  │  通道3: Audio RTCP                  │   │   │
│  │  └──────────────────────────────────────┘   │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
                        │
                        │ 单条TCP连接
                        │ 通过通道ID区分
                        │
┌───────────────────────▼─────────────────────────────┐
│                    服务器                            │
│  ┌──────────────────────────────────────────────┐   │
│  │      RTSP/RTP 统一TCP连接 (端口554)          │   │
│  │                                               │   │
│  │  ┌──────────────────────────────────────┐   │   │
│  │  │  TCP连接内部通道复用                 │   │   │
│  │  │                                      │   │   │
│  │  │  通道0: Video RTP                   │   │   │
│  │  │  通道1: Video RTCP                  │   │   │
│  │  │  通道2: Audio RTP                   │   │   │
│  │  │  通道3: Audio RTCP                  │   │   │
│  │  └──────────────────────────────────────┘   │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

数据包格式:
  +--------+--------+--------+--------+--------+---...---+
  |  '$'   |通道ID  |   长度(2字节)    |   RTP数据      |
  +--------+--------+--------+--------+--------+---...---+
  0        1        2        3        4        ...
数据包格式

在TCP交错模式下,RTP/RTCP数据包以特殊格式嵌入RTSP连接:

复制代码
+--------+--------+--------+--------+--------+---...---+
|  '$'   |通道ID  |   长度(2字节)    |   RTP数据      |
+--------+--------+--------+--------+--------+---...---+
0        1        2        3        4        ...
SETUP请求示例
http 复制代码
SETUP rtsp://server/stream/track1 RTSP/1.0
CSeq: 3
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
User-Agent: FFmpeg
服务器响应
http 复制代码
RTSP/1.0 200 OK
CSeq: 3
Session: 12345678;timeout=60
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
核心代码
c 复制代码
// TCP模式的数据接收
int ff_rtsp_tcp_read_packet(AVFormatContext *s, RTSPStream **prtsp_st,
                            uint8_t *buf, int buf_size)
{
    RTSPState *rt = s->priv_data;
    int id, len, i, ret;
    RTSPStream *rtsp_st;
    
redo:
    for (;;) {
        RTSPMessageHeader reply;
        
        // 尝试读取RTSP回复或等待'$'标记
        ret = ff_rtsp_read_reply(s, &reply, NULL, 1, NULL);
        if (ret < 0)
            return ret;
        if (ret == 1)  // 收到'$',表示后面是RTP数据
            break;
    }
    
    // 读取3字节头:'$' + 通道ID + 长度
    ret = ffurl_read_complete(rt->rtsp_hd, buf, 3);
    if (ret != 3)
        return AVERROR(EIO);
    
    id = buf[0];                    // 通道ID
    len = AV_RB16(buf + 1);        // 数据长度(大端序)
    
    if (len > buf_size || len < 8)
        goto redo;
    
    // 读取实际RTP/RTCP数据
    ret = ffurl_read_complete(rt->rtsp_hd, buf, len);
    if (ret != len)
        return AVERROR(EIO);
    
    // 根据通道ID找到对应的流
    for (i = 0; i < rt->nb_rtsp_streams; i++) {
        rtsp_st = rt->rtsp_streams[i];
        if (id >= rtsp_st->interleaved_min &&
            id <= rtsp_st->interleaved_max)
            goto found;
    }
    goto redo;
    
found:
    *prtsp_st = rtsp_st;
    return len;
}

6.5 传输模式选择策略

FFmpeg自动选择传输模式的策略:

c 复制代码
// 优先级顺序(可通过rtsp_transport选项修改)
int lower_transport_mask = rt->lower_transport_mask;

// 如果设置了prefer_tcp标志
if (rt->rtsp_flags & RTSP_FLAG_PREFER_TCP) {
    // 优先尝试TCP
    ret = ff_rtsp_make_setup_request(s, host, port, 
                                     RTSP_LOWER_TRANSPORT_TCP, 
                                     rt->real_challenge);
    if (ret == 0)
        goto setup_done;
}

// 尝试UDP
if (lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_UDP)) {
    ret = ff_rtsp_make_setup_request(s, host, port, 
                                     RTSP_LOWER_TRANSPORT_UDP, 
                                     rt->real_challenge);
    if (ret == 0)
        goto setup_done;
}

// UDP失败后回退到TCP
if (lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) {
    ret = ff_rtsp_make_setup_request(s, host, port, 
                                     RTSP_LOWER_TRANSPORT_TCP, 
                                     rt->real_challenge);
}

6.6 UDP超时自动切换TCP

FFmpeg实现了智能的容错机制:

c 复制代码
// 文件: rtspdec.c - rtsp_read_packet()
ret = ff_rtsp_fetch_packet(s, pkt);

if (ret < 0) {
    // UDP超时且还没收到任何包
    if (ret == AVERROR(ETIMEDOUT) && !rt->packets) {
        // 如果支持TCP且当前是UDP
        if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP &&
            rt->lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) {
            
            av_log(s, AV_LOG_WARNING, "UDP timeout, retrying with TCP\n");
            
            // 1. 暂停
            if (rtsp_read_pause(s) != 0)
                return -1;
            
            // 2. TEARDOWN(Real服务器需要)
            if (rt->server_type == RTSP_SERVER_REAL)
                ff_rtsp_send_cmd(s, "TEARDOWN", rt->control_uri, NULL, &reply, NULL);
            
            // 3. 清空Session ID,重新SETUP
            rt->session_id[0] = '\0';
            
            // 4. 使用TCP重新SETUP
            if (resetup_tcp(s) == 0) {
                rt->state = RTSP_STATE_IDLE;
                rt->need_subscription = 1;
                
                // 5. 重新PLAY
                if (rtsp_read_play(s) != 0)
                    return -1;
                
                goto retry;
            }
        }
    }
    return ret;
}

7. 关键函数源码解析

7.1 ff_rtsp_send_cmd - 发送RTSP命令

c 复制代码
// 文件: rtsp.c
int ff_rtsp_send_cmd(AVFormatContext *s, const char *method,
                     const char *url, const char *headers,
                     RTSPMessageHeader *reply, unsigned char **content_ptr)
{
    return ff_rtsp_send_cmd_with_content(s, method, url, headers,
                                         reply, content_ptr, NULL, 0);
}

int ff_rtsp_send_cmd_with_content(AVFormatContext *s,
                                  const char *method, const char *url,
                                  const char *headers,
                                  RTSPMessageHeader *reply,
                                  unsigned char **content_ptr,
                                  const unsigned char *send_content,
                                  int send_content_length)
{
    RTSPState *rt = s->priv_data;
    char buf[MAX_URL_SIZE];
    int len;
    
    // 1. 构造请求行
    snprintf(buf, sizeof(buf), "%s %s RTSP/1.0\r\n", method, url);
    
    // 2. 添加CSeq
    rt->seq++;
    av_strlcatf(buf, sizeof(buf), "CSeq: %d\r\n", rt->seq);
    
    // 3. 添加User-Agent
    av_strlcatf(buf, sizeof(buf), "User-Agent: %s\r\n",
                rt->user_agent ? rt->user_agent : LIBAVFORMAT_IDENT);
    
    // 4. 添加Session(如果已建立)
    if (rt->session_id[0] != '\0')
        av_strlcatf(buf, sizeof(buf), "Session: %s\r\n", rt->session_id);
    
    // 5. 添加自定义头
    if (headers)
        av_strlcat(buf, headers, sizeof(buf));
    
    // 6. 添加Content-Length(如果有body)
    if (send_content) {
        av_strlcatf(buf, sizeof(buf), "Content-Length: %d\r\n", 
                   send_content_length);
    }
    
    // 7. 结束头部
    av_strlcat(buf, "\r\n", sizeof(buf));
    
    // 8. 发送请求
    av_log(s, AV_LOG_DEBUG, "Sending:\n%s", buf);
    ffurl_write(rt->rtsp_hd_out, buf, strlen(buf));
    
    // 9. 发送body(如果有)
    if (send_content)
        ffurl_write(rt->rtsp_hd_out, send_content, send_content_length);
    
    // 10. 更新最后命令时间
    rt->last_cmd_time = av_gettime_relative();
    
    // 11. 读取响应
    return ff_rtsp_read_reply(s, reply, content_ptr, 0, method);
}

完整请求示例

http 复制代码
PLAY rtsp://192.168.1.100:554/stream RTSP/1.0
CSeq: 5
User-Agent: FFmpeg/8.0
Session: 12345678
Range: npt=0.000-

7.2 ff_rtsp_read_reply - 读取RTSP响应

c 复制代码
// 文件: rtsp.c (简化版)
int ff_rtsp_read_reply(AVFormatContext *s, RTSPMessageHeader *reply,
                       unsigned char **content_ptr,
                       int return_on_interleaved_data, 
                       const char *method)
{
    RTSPState *rt = s->priv_data;
    char buf[MAX_URL_SIZE];
    int ret, content_length;
    
    memset(reply, 0, sizeof(*reply));
    
    // 1. 读取状态行
    ret = ff_rtsp_read_line(s, buf, sizeof(buf));
    if (ret < 0)
        return ret;
    
    // 检查是否是RTP数据(TCP交错模式)
    if (buf[0] == '$') {
        if (return_on_interleaved_data)
            return 1;  // 告诉调用者:这是RTP数据
        else
            ff_rtsp_skip_packet(s);  // 跳过RTP包
    }
    
    // 2. 解析状态行:"RTSP/1.0 200 OK"
    sscanf(buf, "RTSP/%*d.%*d %d", &reply->status_code);
    
    // 3. 读取所有头部行
    do {
        ret = ff_rtsp_read_line(s, buf, sizeof(buf));
        if (ret < 0)
            return ret;
        
        if (buf[0] == '\0')  // 空行表示头部结束
            break;
        
        // 4. 解析各种头部字段
        ff_rtsp_parse_line(s, reply, buf, rt, method);
        
    } while (1);
    
    // 5. 读取消息体(如果有Content-Length)
    if (reply->content_length > 0 && content_ptr) {
        *content_ptr = av_malloc(reply->content_length + 1);
        
        ret = ffurl_read_complete(rt->rtsp_hd, *content_ptr, 
                                 reply->content_length);
        if (ret < reply->content_length) {
            av_freep(content_ptr);
            return AVERROR(EIO);
        }
        
        (*content_ptr)[reply->content_length] = '\0';
    }
    
    return 0;
}

7.3 ff_rtsp_parse_line - 解析响应头

c 复制代码
// 文件: rtsp.c (核心字段解析)
void ff_rtsp_parse_line(AVFormatContext *s, RTSPMessageHeader *reply,
                        const char *buf, RTSPState *rt, const char *method)
{
    const char *p;
    
    // CSeq解析
    if (av_stristart(buf, "CSeq:", &p)) {
        reply->seq = strtol(p, NULL, 10);
    }
    
    // Content-Length解析
    else if (av_stristart(buf, "Content-Length:", &p)) {
        reply->content_length = strtol(p, NULL, 10);
    }
    
    // Content-Type解析
    else if (av_stristart(buf, "Content-Type:", &p)) {
        p += strspn(p, SPACE_CHARS);
        av_strlcpy(reply->content_type, p, sizeof(reply->content_type));
    }
    
    // Session解析:"Session: 12345678;timeout=60"
    else if (av_stristart(buf, "Session:", &p)) {
        char *q;
        p += strspn(p, SPACE_CHARS);
        q = reply->session_id;
        while (*p && *p != ';' && *p != '\r' && *p != '\n')
            *q++ = *p++;
        *q = '\0';
        
        // 解析timeout参数
        if (av_stristr(p, "timeout=")) {
            sscanf(av_stristr(p, "timeout="), "timeout=%d", &reply->timeout);
        }
    }
    
    // Transport解析(最复杂)
    else if (av_stristart(buf, "Transport:", &p)) {
        ff_rtsp_parse_transport(reply, p);
    }
    
    // RTP-Info解析
    else if (av_stristart(buf, "RTP-Info:", &p)) {
        ff_rtsp_parse_rtp_info(rt, p);
    }
    
    // Range解析:"Range: npt=0.000-123.456"
    else if (av_stristart(buf, "Range:", &p)) {
        ff_rtsp_parse_range_npt(p, &reply->range_start, &reply->range_end);
    }
    
    // Server解析
    else if (av_stristart(buf, "Server:", &p)) {
        p += strspn(p, SPACE_CHARS);
        av_strlcpy(reply->server, p, sizeof(reply->server));
        
        // 检测服务器类型
        if (av_stristr(p, "WMServer"))
            rt->server_type = RTSP_SERVER_WMS;
        else if (av_stristr(p, "RealServer") || av_stristr(p, "Helix"))
            rt->server_type = RTSP_SERVER_REAL;
    }
    
    // WWW-Authenticate解析(认证)
    else if (av_stristart(buf, "WWW-Authenticate:", &p) ||
             av_stristart(buf, "Proxy-Authenticate:", &p)) {
        ff_http_auth_handle_header(&rt->auth_state, "WWW-Authenticate", p);
    }
}

7.4 ff_rtsp_fetch_packet - 获取数据包

c 复制代码
// 文件: rtsp.c (简化逻辑)
int ff_rtsp_fetch_packet(AVFormatContext *s, AVPacket *pkt)
{
    RTSPState *rt = s->priv_data;
    int ret;
    RTSPStream *rtsp_st;
    
    // TCP交错模式
    if (rt->lower_transport == RTSP_LOWER_TRANSPORT_TCP) {
        uint8_t buf[RTP_MAX_PACKET_LENGTH];
        
        // 读取一个RTP包
        ret = ff_rtsp_tcp_read_packet(s, &rtsp_st, buf, sizeof(buf));
        if (ret < 0)
            return ret;
        
        // 解析RTP包
        if (rt->transport == RTSP_TRANSPORT_RTP) {
            ret = ff_rtp_parse_packet(rtsp_st->transport_priv, pkt, buf, ret);
        } else if (rt->transport == RTSP_TRANSPORT_RDT) {
            ret = ff_rdt_parse_packet(rtsp_st->transport_priv, pkt, buf, ret);
        }
        
        return ret;
    }
    
    // UDP模式
    else {
        // 轮询所有UDP socket
        int max_p = rt->max_p;
        struct pollfd *p = rt->p;
        int64_t wait_end = av_gettime_relative() + rt->stimeout;
        
        for (;;) {
            // 计算剩余超时时间
            int64_t timeout = wait_end - av_gettime_relative();
            if (timeout < 0)
                return AVERROR(ETIMEDOUT);
            
            // poll等待数据
            ret = poll(p, max_p, timeout / 1000);
            if (ret < 0)
                return AVERROR(errno);
            if (ret == 0)
                return AVERROR(ETIMEDOUT);
            
            // 检查哪个socket有数据
            for (int i = 0; i < max_p; i++) {
                if (p[i].revents & POLLIN) {
                    rtsp_st = rt->rtsp_streams[i];
                    
                    // 从UDP socket读取
                    ret = ffurl_read(rtsp_st->rtp_handle, 
                                    rt->recvbuf, RECVBUF_SIZE);
                    if (ret < 0)
                        continue;
                    
                    // 解析RTP
                    ret = ff_rtp_parse_packet(rtsp_st->transport_priv, 
                                             pkt, rt->recvbuf, ret);
                    if (ret < 0)
                        continue;
                    
                    return ret;
                }
            }
        }
    }
}

7.5 rtsp_read_seek - Seek实现

c 复制代码
// 文件: rtspdec.c
static int rtsp_read_seek(AVFormatContext *s, int stream_index,
                          int64_t timestamp, int flags)
{
    RTSPState *rt = s->priv_data;
    int ret;
    
    // 1. 转换时间戳为AV_TIME_BASE单位
    rt->seek_timestamp = av_rescale_q(timestamp,
                                      s->streams[stream_index]->time_base,
                                      AV_TIME_BASE_Q);
    
    // 2. 根据当前状态执行不同操作
    switch(rt->state) {
    case RTSP_STATE_IDLE:
        // 空闲状态不需要操作
        break;
        
    case RTSP_STATE_STREAMING:
        // 流媒体中:先PAUSE,再PLAY(带Range参数)
        if ((ret = rtsp_read_pause(s)) != 0)
            return ret;
        
        rt->state = RTSP_STATE_SEEKING;
        
        if ((ret = rtsp_read_play(s)) != 0)
            return ret;
        break;
        
    case RTSP_STATE_PAUSED:
        // 暂停状态:直接设置为IDLE,下次PLAY会带Range
        rt->state = RTSP_STATE_IDLE;
        break;
    }
    
    return 0;
}

Seek时的PLAY命令

http 复制代码
PLAY rtsp://192.168.1.100:554/stream RTSP/1.0
CSeq: 8
User-Agent: FFmpeg/8.0
Session: 12345678
Range: npt=30.500-

8. RTSP服务端监听模式

8.1 监听模式概述

FFmpeg不仅支持RTSP客户端拉流,还支持作为RTSP服务器接收推流。这种模式主要用于:

  • 接收实时编码器推送的流
  • 作为流媒体网关
  • 测试和开发

8.2 监听模式流程

RTSP服务端监听模式时序流程

复制代码
┌─────────────┐                                    ┌─────────────┐
│  推流端     │                                    │ FFmpeg     │
│ (编码器)    │                                    │ RTSP服务器 │
└──────┬──────┘                                    └──────┬──────┘
       │                                                   │
       │  [启动监听模式: ffmpeg -rtsp_flags listen]       │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段1: 服务器启动监听                        │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │                             绑定TCP端口554        │
       │                             进入等待连接状态      │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段2: 推流端发起连接                        │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──TCP连接────────────────────────────────────────>│
       │                                                   │
       │──OPTIONS rtsp://server/live─────────────────────>│
       │<──200 OK                                         │
       │    Public: ANNOUNCE,PAUSE,SETUP,TEARDOWN,RECORD │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段3: 公告媒体描述                         │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──ANNOUNCE rtsp://server/live────────────────────>│
       │    Content-Type: application/sdp                 │
       │    [SDP描述]                                     │
       │                              [解析SDP,创建媒体流]│
       │<──200 OK                                         │
       │                              State: IDLE → PAUSED│
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段4: 建立传输通道                         │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──SETUP rtsp://server/live/track1───────────────>│
       │    Transport: RTP/AVP;unicast;client_port=5000-5001│
       │                              [打开本地UDP端口]   │
       │<──200 OK                                         │
       │    Session: 12345678                             │
       │    Transport: ...;server_port=6000-6001          │
       │                                                   │
       │──SETUP rtsp://server/live/track2───────────────>│
       │    Transport: RTP/AVP;unicast;client_port=5002-5003│
       │<──200 OK                                         │
       │    Session: 12345678                             │
       │    Transport: ...;server_port=6002-6003          │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段5: 开始录制                             │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──RECORD rtsp://server/live─────────────────────>│
       │    Session: 12345678                             │
       │<──200 OK                                         │
       │                              State: PAUSED → STREAMING│
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段6: 数据传输(持续)                     │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──RTP数据包──────────────────────────────────────>│
       │──RTP数据包──────────────────────────────────────>│
       │──RTP数据包──────────────────────────────────────>│
       │    ... (持续传输)                                │
       │                                                   │
       │  ┌────────────────────────────────────────────┐  │
       │  │ 阶段7: 结束会话                             │  │
       │  └────────────────────────────────────────────┘  │
       │                                                   │
       │──TEARDOWN rtsp://server/live───────────────────>│
       │    Session: 12345678                             │
       │<──200 OK                                         │
       │                              State: STREAMING → IDLE│
       │                                                   │

监听模式时序表

阶段 推流端操作 FFmpeg服务器响应 状态变化
1. 启动 - 绑定TCP端口554 进入等待状态 -
2. 连接 TCP连接 接受连接 -
3. 能力查询 OPTIONS请求 200 OK Public: ANNOUNCE,PAUSE,SETUP,TEARDOWN,RECORD -
4. 公告 ANNOUNCE请求 Content-Type: application/sdp [SDP描述] 200 OK 解析SDP,创建媒体流 IDLE → PAUSED
5. 建立传输 SETUP track1 Transport: RTP/AVP;unicast;client_port=5000-5001 200 OK Session: 12345678 打开本地UDP端口 Transport: ...;server_port=6000-6001 -
SETUP track2 Transport: RTP/AVP;unicast;client_port=5002-5003 200 OK Session: 12345678 Transport: ...;server_port=6002-6003 -
6. 开始录制 RECORD请求 Session: 12345678 200 OK PAUSED → STREAMING
7. 数据传输 持续发送RTP数据包 接收并处理数据包 -
8. 结束 TEARDOWN请求 Session: 12345678 200 OK STREAMING → IDLE

8.3 rtsp_listen核心实现

c 复制代码
// 文件: rtspdec.c
static int rtsp_listen(AVFormatContext *s)
{
    RTSPState *rt = s->priv_data;
    char proto[128], host[128], path[512], auth[128];
    char uri[500];
    int port;
    int default_port = RTSP_DEFAULT_PORT;
    char tcpname[500];
    const char *lower_proto = "tcp";
    
    // 1. 解析URL
    av_url_split(proto, sizeof(proto), auth, sizeof(auth), 
                 host, sizeof(host), &port, path, sizeof(path), s->url);
    
    // 2. 构造控制URI
    ff_url_join(rt->control_uri, sizeof(rt->control_uri), 
                proto, NULL, host, port, "%s", path);
    
    // 3. 支持RTSPS(RTSP over TLS)
    if (!strcmp(proto, "rtsps")) {
        lower_proto = "tls";
        default_port = RTSPS_DEFAULT_PORT;
    }
    
    if (port < 0)
        port = default_port;
    
    // 4. 创建监听TCP连接
    ff_url_join(tcpname, sizeof(tcpname), lower_proto, NULL, host, port,
                "?listen&listen_timeout=%d", rt->initial_timeout * 1000);
    
    ret = ffurl_open_whitelist(&rt->rtsp_hd, tcpname, AVIO_FLAG_READ_WRITE,
                               &s->interrupt_callback, NULL,
                               s->protocol_whitelist, 
                               s->protocol_blacklist, NULL);
    if (ret < 0) {
        av_log(s, AV_LOG_ERROR, "Unable to open RTSP for listening\n");
        goto fail;
    }
    
    // 5. 初始化状态
    rt->state = RTSP_STATE_IDLE;
    rt->rtsp_hd_out = rt->rtsp_hd;
    
    // 6. 主循环:等待并处理RTSP命令
    for (;;) {
        unsigned char rbuf[MAX_URL_SIZE];
        unsigned char method[10];
        int rbuflen = 0;
        enum RTSPMethod methodcode;
        
        // 读取请求行
        ret = read_line(s, rbuf, sizeof(rbuf), &rbuflen);
        if (ret < 0)
            goto fail;
        
        // 解析命令
        ret = parse_command_line(s, rbuf, rbuflen, uri, sizeof(uri), 
                                method, sizeof(method), &methodcode);
        if (ret) {
            av_log(s, AV_LOG_ERROR, "RTSP: Unexpected Command\n");
            goto fail;
        }
        
        // 根据方法分发处理
        if (methodcode == ANNOUNCE) {
            ret = rtsp_read_announce(s);
            rt->state = RTSP_STATE_PAUSED;
        } 
        else if (methodcode == OPTIONS) {
            ret = rtsp_read_options(s);
        } 
        else if (methodcode == RECORD) {
            ret = rtsp_read_record(s);
            if (!ret)
                return 0;  // 准备好接收数据,退出监听循环
        } 
        else if (methodcode == SETUP) {
            ret = rtsp_read_setup(s, host, uri);
        }
        
        if (ret) {
            ret = AVERROR_INVALIDDATA;
            goto fail;
        }
    }
    
fail:
    ff_rtsp_close_streams(s);
    ff_rtsp_close_connections(s);
    ff_network_close();
    return ret;
}

8.4 ANNOUNCE处理

c 复制代码
// 文件: rtspdec.c
static int rtsp_read_announce(AVFormatContext *s)
{
    RTSPState *rt = s->priv_data;
    RTSPMessageHeader request = { 0 };
    char *sdp;
    int ret;
    
    // 1. 读取请求头
    ret = rtsp_read_request(s, &request, "ANNOUNCE");
    if (ret)
        return ret;
    rt->seq++;
    
    // 2. 检查Content-Type
    if (strcmp(request.content_type, "application/sdp")) {
        av_log(s, AV_LOG_ERROR, "Unexpected content type %s\n",
               request.content_type);
        rtsp_send_reply(s, RTSP_STATUS_SERVICE, NULL, request.seq);
        return AVERROR_OPTION_NOT_FOUND;
    }
    
    // 3. 读取SDP内容
    if (request.content_length) {
        sdp = av_malloc(request.content_length + 1);
        if (!sdp)
            return AVERROR(ENOMEM);
        
        if (ffurl_read_complete(rt->rtsp_hd, sdp, request.content_length)
            < request.content_length) {
            av_log(s, AV_LOG_ERROR,
                   "Unable to get complete SDP Description in ANNOUNCE\n");
            rtsp_send_reply(s, RTSP_STATUS_INTERNAL, NULL, request.seq);
            av_free(sdp);
            return AVERROR(EIO);
        }
        
        sdp[request.content_length] = '\0';
        av_log(s, AV_LOG_VERBOSE, "SDP: %s\n", sdp);
        
        // 4. 解析SDP
        ret = ff_sdp_parse(s, sdp);
        av_free(sdp);
        
        if (ret)
            return ret;
        
        // 5. 发送200 OK
        rtsp_send_reply(s, RTSP_STATUS_OK, NULL, request.seq);
        return 0;
    }
    
    av_log(s, AV_LOG_ERROR, "Content-Length header missing\n");
    rtsp_send_reply(s, RTSP_STATUS_INTERNAL,
                    "Content-Length exceeds buffer size", request.seq);
    return AVERROR(EIO);
}

8.5 SETUP处理(服务端)

c 复制代码
// 文件: rtspdec.c
static int rtsp_read_setup(AVFormatContext *s, char* host, char *controlurl)
{
    RTSPState *rt = s->priv_data;
    RTSPMessageHeader request = { 0 };
    RTSPStream *rtsp_st;
    char responseheaders[MAX_URL_SIZE];
    int localport = -1;
    int streamid = 0;
    
    // 1. 读取SETUP请求
    ret = rtsp_read_request(s, &request, "SETUP");
    if (ret)
        return ret;
    rt->seq++;
    
    // 2. 检查Transport
    if (!request.nb_transports) {
        av_log(s, AV_LOG_ERROR, "No transport defined in SETUP\n");
        return AVERROR_INVALIDDATA;
    }
    
    // 3. 验证transport参数
    for (int i = 0; i < request.nb_transports; i++) {
        if (!request.transports[i].mode_record ||
            (request.transports[i].lower_transport != RTSP_LOWER_TRANSPORT_UDP &&
             request.transports[i].lower_transport != RTSP_LOWER_TRANSPORT_TCP)) {
            av_log(s, AV_LOG_ERROR, "mode=record not set or transport"
                   " protocol not supported\n");
            return AVERROR_INVALIDDATA;
        }
    }
    
    // 4. 查找对应的流
    for (streamid = 0; streamid < rt->nb_rtsp_streams; streamid++) {
        if (!strcmp(rt->rtsp_streams[streamid]->control_url, controlurl))
            break;
    }
    
    if (streamid == rt->nb_rtsp_streams) {
        av_log(s, AV_LOG_ERROR, "Unable to find requested track\n");
        return AVERROR_STREAM_NOT_FOUND;
    }
    
    rtsp_st = rt->rtsp_streams[streamid];
    localport = rt->rtp_port_min;
    
    // 5. TCP模式
    if (request.transports[0].lower_transport == RTSP_LOWER_TRANSPORT_TCP) {
        rt->lower_transport = RTSP_LOWER_TRANSPORT_TCP;
        
        if ((ret = ff_rtsp_open_transport_ctx(s, rtsp_st))) {
            rtsp_send_reply(s, RTSP_STATUS_TRANSPORT, NULL, request.seq);
            return ret;
        }
        
        rtsp_st->interleaved_min = request.transports[0].interleaved_min;
        rtsp_st->interleaved_max = request.transports[0].interleaved_max;
        
        snprintf(responseheaders, sizeof(responseheaders), 
                 "Transport: RTP/AVP/TCP;unicast;mode=record;interleaved=%d-%d\r\n",
                 request.transports[0].interleaved_min,
                 request.transports[0].interleaved_max);
    }
    // 6. UDP模式
    else {
        do {
            AVDictionary *opts = NULL;
            av_dict_set_int(&opts, "buffer_size", rt->buffer_size, 0);
            
            char url[MAX_URL_SIZE];
            ff_url_join(url, sizeof(url), "rtp", NULL, host, localport, NULL);
            
            ret = ffurl_open_whitelist(&rtsp_st->rtp_handle, url, 
                                      AVIO_FLAG_READ_WRITE,
                                      &s->interrupt_callback, &opts,
                                      s->protocol_whitelist, 
                                      s->protocol_blacklist, NULL);
            av_dict_free(&opts);
            
            if (ret)
                localport += 2;
        } while (ret || localport > rt->rtp_port_max);
        
        if (localport > rt->rtp_port_max) {
            rtsp_send_reply(s, RTSP_STATUS_TRANSPORT, NULL, request.seq);
            return ret;
        }
        
        if ((ret = ff_rtsp_open_transport_ctx(s, rtsp_st))) {
            rtsp_send_reply(s, RTSP_STATUS_TRANSPORT, NULL, request.seq);
            return ret;
        }
        
        localport = ff_rtp_get_local_rtp_port(rtsp_st->rtp_handle);
        
        snprintf(responseheaders, sizeof(responseheaders), 
                 "Transport: RTP/AVP/UDP;unicast;mode=record;source=%s;"
                 "client_port=%d-%d;server_port=%d-%d\r\n",
                 host, 
                 request.transports[0].client_port_min,
                 request.transports[0].client_port_max, 
                 localport, localport + 1);
    }
    
    // 7. 生成Session ID(RFC 2326要求至少8位)
    while (strlen(rt->session_id) < 8)
        av_strlcatf(rt->session_id, 512, "%u", av_get_random_seed());
    
    av_strlcatf(responseheaders, sizeof(responseheaders), 
                "Session: %s\r\n", rt->session_id);
    
    // 8. 发送响应
    rtsp_send_reply(s, RTSP_STATUS_OK, responseheaders, request.seq);
    
    rt->state = RTSP_STATE_PAUSED;
    return 0;
}

8.6 状态码定义

c 复制代码
// RTSP状态码
enum RTSPStatusCode {
    RTSP_STATUS_OK           = 200, // 成功
    RTSP_STATUS_METHOD       = 405, // 方法不允许
    RTSP_STATUS_BANDWIDTH    = 453, // 带宽不足
    RTSP_STATUS_SESSION      = 454, // 会话未找到
    RTSP_STATUS_STATE        = 455, // 方法在此状态无效
    RTSP_STATUS_AGGREGATE    = 459, // 不允许聚合操作
    RTSP_STATUS_ONLY_AGGREGATE = 460, // 仅允许聚合操作
    RTSP_STATUS_TRANSPORT    = 461, // 不支持的传输
    RTSP_STATUS_INTERNAL     = 500, // 内部服务器错误
    RTSP_STATUS_SERVICE      = 503, // 服务不可用
    RTSP_STATUS_VERSION      = 505, // RTSP版本不支持
};

// 状态消息映射
static const struct RTSPStatusMessage {
    enum RTSPStatusCode code;
    const char *message;
} status_messages[] = {
    { RTSP_STATUS_OK,             "OK"                               },
    { RTSP_STATUS_METHOD,         "Method Not Allowed"               },
    { RTSP_STATUS_BANDWIDTH,      "Not Enough Bandwidth"             },
    { RTSP_STATUS_SESSION,        "Session Not Found"                },
    { RTSP_STATUS_STATE,          "Method Not Valid in This State"   },
    { RTSP_STATUS_AGGREGATE,      "Aggregate operation not allowed"  },
    { RTSP_STATUS_ONLY_AGGREGATE, "Only aggregate operation allowed" },
    { RTSP_STATUS_TRANSPORT,      "Unsupported transport"            },
    { RTSP_STATUS_INTERNAL,       "Internal Server Error"            },
    { RTSP_STATUS_SERVICE,        "Service Unavailable"              },
    { RTSP_STATUS_VERSION,        "RTSP Version not supported"       },
    { 0,                          "NULL"                             }
};

9. 错误处理与容错机制

9.1 错误处理策略

错误类型 处理策略 实现位置
网络超时 UDP→TCP自动切换 rtsp_read_packet()
连接断开 返回EOF ff_rtsp_read_reply()
认证失败 重试3次 ff_rtsp_send_cmd()
SDP解析失败 返回错误 ff_sdp_parse()
端口占用 尝试下一个端口 rtsp_read_setup()
服务器错误(5xx) 映射到AVERROR ff_rtsp_averror()

9.2 UDP超时处理详解

c 复制代码
// UDP超时后的完整恢复流程
if (ret == AVERROR(ETIMEDOUT) && !rt->packets) {
    if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP &&
        rt->lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) {
        
        av_log(s, AV_LOG_WARNING, "UDP timeout, retrying with TCP\n");
        
        // 步骤1: PAUSE当前播放
        if (rtsp_read_pause(s) != 0)
            return -1;
        
        // 步骤2: TEARDOWN(Real Server特殊要求)
        if (rt->server_type == RTSP_SERVER_REAL) {
            RTSPMessageHeader reply;
            ff_rtsp_send_cmd(s, "TEARDOWN", rt->control_uri, NULL, &reply, NULL);
        }
        
        // 步骤3: 清空Session ID
        rt->session_id[0] = '\0';
        
        // 步骤4: 使用TCP重新SETUP所有流
        if (resetup_tcp(s) == 0) {
            rt->state = RTSP_STATE_IDLE;
            rt->need_subscription = 1;
            
            // 步骤5: 重新PLAY
            if (rtsp_read_play(s) != 0)
                return -1;
            
            goto retry;
        }
    }
}

9.3 保活机制

c 复制代码
// 防止服务器超时断开连接
if (!(rt->rtsp_flags & RTSP_FLAG_LISTEN)) {
    int64_t elapsed = (av_gettime_relative() - rt->last_cmd_time) / 1000000;
    
    // 在超时时间的一半发送保活命令
    if (elapsed >= rt->timeout / 2 || rt->auth_state.stale) {
        if (rt->server_type == RTSP_SERVER_WMS ||
            (rt->server_type != RTSP_SERVER_REAL && 
             rt->get_parameter_supported)) {
            // WMS或支持GET_PARAMETER的服务器
            ff_rtsp_send_cmd_async(s, "GET_PARAMETER", rt->control_uri, NULL);
        } else {
            // 其他服务器使用OPTIONS
            ff_rtsp_send_cmd_async(s, "OPTIONS", rt->control_uri, NULL);
        }
        
        rt->auth_state.stale = 0;
    }
}

9.4 认证处理

FFmpeg支持Basic和Digest两种认证:

c 复制代码
// 发送带认证的请求
int ff_rtsp_send_cmd_with_auth(AVFormatContext *s, const char *method,
                               const char *url, const char *headers,
                               RTSPMessageHeader *reply,
                               unsigned char **content_ptr)
{
    RTSPState *rt = s->priv_data;
    char auth[1024];
    int ret;
    
    // 第一次尝试(可能没有认证)
    ret = ff_rtsp_send_cmd(s, method, url, headers, reply, content_ptr);
    
    // 收到401 Unauthorized
    if (reply->status_code == 401) {
        // 构造认证头
        if (ff_http_auth_create_response(&rt->auth_state, "Authorization", 
                                         url, method, auth, sizeof(auth))) {
            char new_headers[2048];
            
            // 添加认证头
            snprintf(new_headers, sizeof(new_headers), 
                    "%sAuthorization: %s\r\n",
                    headers ? headers : "", auth);
            
            // 重试
            ret = ff_rtsp_send_cmd(s, method, url, new_headers, 
                                  reply, content_ptr);
        }
    }
    
    return ret;
}

10. 性能优化与实践建议

10.1 参数优化建议

参数 默认值 推荐值 说明
buffer_size -1(系统默认) 2097152 (2MB) UDP接收缓冲区,防止丢包
reorder_queue_size -1 500 RTP重排序队列大小
timeout 0 5000000 (5秒) Socket I/O超时(微秒)
stimeout 0 5000000 (5秒) 包接收超时(微秒)
max_delay 0.5秒 0.1秒 最大缓冲延迟

10.2 命令行使用示例

基本拉流
bash 复制代码
# UDP模式(默认)
ffplay rtsp://192.168.1.100:554/stream

# 强制TCP模式
ffplay -rtsp_transport tcp rtsp://192.168.1.100:554/stream

# 指定缓冲区大小
ffplay -buffer_size 4194304 rtsp://192.168.1.100:554/stream
高级配置
bash 复制代码
# 低延迟配置
ffplay -rtsp_transport tcp \
       -fflags nobuffer \
       -flags low_delay \
       -max_delay 100000 \
       rtsp://192.168.1.100:554/stream

# 高可靠性配置(大缓冲)
ffplay -rtsp_transport tcp \
       -buffer_size 8388608 \
       -reorder_queue_size 1000 \
       rtsp://192.168.1.100:554/stream

# 监听模式(接收推流)
ffmpeg -rtsp_flags listen \
       -i rtsp://0.0.0.0:8554/live \
       -c copy output.mp4

10.3 代码集成示例

c 复制代码
#include <libavformat/avformat.h>

int main() {
    AVFormatContext *fmt_ctx = NULL;
    AVDictionary *opts = NULL;
    int ret;
    
    // 设置RTSP选项
    av_dict_set(&opts, "rtsp_transport", "tcp", 0);
    av_dict_set(&opts, "buffer_size", "4194304", 0);
    av_dict_set_int(&opts, "timeout", 5000000, 0);
    
    // 打开RTSP流
    ret = avformat_open_input(&fmt_ctx, "rtsp://192.168.1.100/stream", 
                              NULL, &opts);
    if (ret < 0) {
        fprintf(stderr, "Could not open input: %s\n", av_err2str(ret));
        return -1;
    }
    
    // 获取流信息
    ret = avformat_find_stream_info(fmt_ctx, NULL);
    if (ret < 0) {
        fprintf(stderr, "Could not find stream info\n");
        return -1;
    }
    
    // 打印流信息
    av_dump_format(fmt_ctx, 0, "rtsp://192.168.1.100/stream", 0);
    
    // 读取数据包
    AVPacket pkt;
    while (av_read_frame(fmt_ctx, &pkt) >= 0) {
        // 处理数据包
        av_packet_unref(&pkt);
    }
    
    // 清理
    avformat_close_input(&fmt_ctx);
    av_dict_free(&opts);
    
    return 0;
}

10.4 性能监控指标

建议在生产环境监控以下指标:

  1. 丢包率: 通过RTCP报告获取
  2. 延迟 : rtpctx->last_rtcp_reception_time - rtpctx->last_rtcp_ntp_time
  3. 带宽: 累计接收字节数 / 时间
  4. 重连次数: UDP→TCP切换次数
  5. 缓冲队列长度 : rtpctx->queue.nb_packets

10.5 常见问题排查

问题 可能原因 解决方案
UDP无法接收数据 NAT/防火墙阻挡 使用TCP模式或配置端口转发
频繁断线重连 网络不稳定 增大buffer_size和timeout
延迟过高 缓冲队列过大 设置-fflags nobuffer -flags low_delay
画面卡顿 丢包严重 检查网络质量,使用TCP
认证失败 用户名密码错误 URL格式:rtsp://user:pass@host/stream

10.6 调试技巧

bash 复制代码
# 启用详细日志
export FFREPORT=file=rtsp_debug.log:level=48
ffplay -v trace rtsp://192.168.1.100/stream

# 抓包分析
tcpdump -i eth0 -w rtsp.pcap port 554 or portrange 5000-5100

# 使用Wireshark过滤
# RTSP: rtsp
# RTP: rtp
# RTCP: rtcp

总结

本文从协议基础、架构设计、流程分析、代码实现等多个维度深入剖析了FFmpeg RTSP拉流的完整实现。核心要点包括:

  1. RTSP是控制协议,实际数据通过RTP传输
  2. 状态机设计是核心,严格管理IDLE/PAUSED/STREAMING状态转换
  3. 传输模式选择需根据场景:UDP低延迟,TCP高可靠
  4. 容错机制完善,支持UDP超时自动切换TCP
  5. 监听模式支持作为RTSP服务器接收推流

FFmpeg的RTSP实现历经多年打磨,已经非常成熟稳定,是学习流媒体协议栈的优秀范例。


参考资料

  1. RFC 2326 - RTSP 1.0规范
  2. RFC 7826 - RTSP 2.0规范
  3. RFC 3550 - RTP协议
  4. FFmpeg官方文档
  5. SDP规范 RFC 4566

💡 如果觉得本文有帮助,欢迎点赞、收藏、分享!

💬 有任何问题欢迎在评论区讨论交流!

相关推荐
IFTICing12 小时前
【环境配置】ffmpeg下载、安装、配置(Windows环境)
windows·ffmpeg
haiy201112 小时前
FFmpeg 编译
ffmpeg
aqi0015 小时前
FFmpeg开发笔记(八十九)基于FFmpeg的直播视频录制工具StreamCap
ffmpeg·音视频·直播·流媒体
八月的雨季 最後的冰吻19 小时前
FFmepg--28- 滤镜处理 YUV 视频帧:实现上下镜像效果
ffmpeg·音视频
ganqiuye19 小时前
向ffmpeg官方源码仓库提交patch
大数据·ffmpeg·video-codec
草明19 小时前
ffmpeg 把 ts 转换成 mp3
ffmpeg
aqi0021 小时前
FFmpeg开发笔记(九十二)基于Kotlin的开源Android推流器StreamPack
android·ffmpeg·kotlin·音视频·直播·流媒体
nuoxin1141 天前
GSV1011-富利威-HDMI芯片选型
arm开发·驱动开发·fpga开发·ffmpeg·射频工程
潇湘秦2 天前
一次微小的CPU波动,你能查到什么?
ffmpeg