FFMPEG&网络推流

FFMPEG&网络推流

FFMPEG

记录ffmpeg的开发学习过程,代码借鉴了许多RV1126的sdk

解复用模块(Demuxer)

解复用模块负责识别文件类型,媒体类型,分离出各媒体原始数据流,打上时钟信息后送给下级decoder filter。其核心是AVFormatContext结构体,它封装了整个媒体文件的信息,管理所有的音视频流,并提供文件格式相关的操作接口。

重要结构体

AVFormatContext

FFMPEG的核心结构体,这个结构体是统领全局的基本结构体,这个结构体最主要作用的是处理封装、解封装等核心功能。封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息

AVOutputFormat

FFMPEG输出结构体

这个结构体的功能类似于COM接口,表示文件容器输出格式,这个结构体的特点是着重于函数的实现

其中比较重要的成员有:

int (*write_header)(struct AVFormatContext *);  //写入数据头部
int (*write_packet)(struct AVFormatContext *, AVPacket *pkt);//写一个数据包。 如果在标志中设置AVFMT_ALLOW_FLUSH,则pkt可以为NULL。
int (*interleave_packet)(struct AVFormatContext *, AVPacket *out, AVPacket *in, int flush); //刷新AVPacket,并写入
write_header:根据不同的流媒体协议去写入头部,如rtmp,rtsp,srt,udp,tcp
                        ↓
write_packet:对不同的流媒体服务器,写入每一帧AVPacket包
                        ↓
interleave_packet:对每一个AVPacket进行刷新

AVStream

AVStream包含了每一个视频/音频流信息的结构体,同样的这个结构体也有很多重要的成员变量

比较重要的结构体如下:(例如时间基,帧率等)

index/id:这个数字是自动生成的,根据index可以从streams表中找到该流
time_base:流的时间基,这是一个实数,该AVStream中流媒体数据的PTS和DTS都将会以这个时间基准。视频时间基准以帧率为基准,音频以采样率为时间基准
start_time:流的起始时间,以流的时间基准为单位,通常是该流的第一个PTS
duration:流的总时间,以流的时间基准为单位
need_parsing:对该流的parsing过程的控制
nb_frames:流内的帧数目
avg_frame_rate:平均帧率
codec:该流对应的AVCodecContext结构,调用avformat_open_input生成
parser:指向该流对应的AVCodecParserContext结构,调用av_find_stream_info生成

AVCodeContext

AVCodecContext是FFMPEG编解码上下文的结构体,它内部包含了AVCodec编解码参数结构体。除了AVCodec结构体外,还有AVCodecInternal、AVRotational结构体,这包含了AVCodecID、AVMediaType、AVPixelFormat、AVSampleFormat等类型,其中包含视频的分辨率width、height、帧率framerate、码率bitrate等。

AVCodec是ffmpeg的音视频编解码,它提供了各种音视频的编码库和解码库,FFMPEG通过AVCODEC可以将音视频数据编码成对应的数据压缩包。

AVCodecID:AVCODECID编码器的ID号,这里的编码器ID包含了视频的编码器ID,如:AV_CODEC_ID_H264、AV_CODEC_ID_H265等等。音频的编码器ID:AV_CODEC_ID_AAC、AV_CODEC_ID_MP3等等。

AVMediaType:指明当前编码器的类型,包括:视频(AVMEDIA_TYPE_VIDEO)、音频(AVMEDIA_TYPE_AUDIO)、字幕(AVMEDIA_TYPE_SUBTITILE)。

AVPacket

AVPacket是FFMPEG中一个非常重要的结构体,它保存了解复用之后,解码之前的数据,它存的是压缩数据的音视频数据。除了压缩数据后,它还包含了一些重要的参数信息,如显示时间戳(pts)、解码时间戳(dts)、数据时长、流媒体索引等

AVPacket如果是存储视频数据的话,基本上是存储H264/H265这些码流,如果存储音频数据的话则会保存AAC/MP3码流。

AVIOContext

AVIOContext结构体是FFMPEG管理输入输出的结构体

复合流和裸流

复合流指的是音频+视频等多种裸流结合到一起的流

H264视频编码码流和AAC音频编码码流都是裸流,因为他们都只有一种流

在FFMPEG进行推流的时候推送的是复合流,即使只有单流,FFMPEG底层也会把数据进行填充让其变成复合流再去推送

FFMPEG输出模块初始化

输出模块的最大作用是对音视频推流模块进行初始化让其能够正常工作起来。FFMPEG输出模块初始化大致流程如下:

  1. avformat_alloc_output_context2分配AVFormatContext
  2. avformat_new_stream初始化AVStream结构体
  3. avcodec_find_encoder找出对应的codec编码器
  4. 利用avcodec_alloc_context3分配AVCodecCotext
  5. 设置AVCodecContext结构体参数
  6. 利用avcodec_parameters_from_contextcodec参数传输到AVStream里面的参数
  7. avio_open初始化FFMPEG的IO结构体
  8. avformat_write_header初始化AVFormatContext

首先自定义一个保存FFMPEG相关配置的结构体

// OutputStream 结构体,用于管理每个媒体流
typedef struct
{
    AVStream* stream;
    AVPacket* pkt;       // 用于在推流循环中复用 AVPacket 对象
    int64_t   next_pts;  // 下一个帧的 PTS (Presentation Timestamp)
    // 新增 extradata 相关成员
    uint8_t* h264_extradata;
    int      h264_extradata_size;
    int      h264_extradata_set;  // 标志位:是否已设置过 extradata
} OutputStream;

// RKMEDIA_FFMPEG_CONFIG 结构体,保存 FFmpeg 相关的配置和上下文
typedef struct
{
    int          width;
    int          height;
    unsigned int config_id;
    int  protocol_type;  // 流媒体协议类型 (FLV_PROTOCOL 或 TS_PROTOCOL)
    char network_addr[NETWORK_ADDR_LENGTH];  // 流媒体地址
    enum AVCodecID   video_codec;    // 视频编码器ID (如 AV_CODEC_ID_H264)
    enum AVCodecID   audio_codec;    // 音频编码器ID (如 AV_CODEC_ID_AAC)
    OutputStream     video_stream;   // 视频流配置
    OutputStream     audio_stream;   // 音频流配置
    AVFormatContext* oc;             // FFmpeg 封装格式上下文
    int              ffmpeg_inited;  // 新增标志,指示 FFmpeg
                        // 推流器是否已完全初始化并准备好推送
} RKMEDIA_FFMPEG_CONFIG;

RKMEDIA_FFMPEG_CONFIG结构体用于管理FFMPEG相关配置及上下文,而OutputStream用于管理媒体流

// 设置基本配置参数
    ffmpeg_config->width         = WIDTH;
    ffmpeg_config->height        = HEIGHT;
    ffmpeg_config->config_id     = 0;             // 可以根据需要设置
    ffmpeg_config->protocol_type = FLV_PROTOCOL;  // 默认为 RTMP (FLV) 协议
    ffmpeg_config->video_codec = AV_CODEC_ID_H264;  // 指定视频编码器为 H.264
    ffmpeg_config->audio_codec =
        AV_CODEC_ID_NONE;  // 指定音频编码器为 AAC (如果不需要音频,设为
                           // AV_CODEC_ID_NONE)

由于对于FFMPEG输出模块的初始化流程是固定的,所以可以直接封装成一个函数

int init_rkmedia_ffmpeg_context(RKMEDIA_FFMPEG_CONFIG* ffmpeg_config)

首先是分配AVFormatContext

int avformat_alloc_output_context2(AVFormatContext **ctx, ff_const59 AVOutputFormat *oformat,
                                   const char *format_name, const char *filename);
  • 第一个传输参数:AVFormatContext结构体指针的指针,是存储音视频封装格式中包含的信息的结构体,所有对文件的封装、编码都是从这个结构体开始。
  • 第二个传输参数:AVOutputFormat的结构体指针,它主要存储复合流信息的常规配置,默认为设置NULL
  • 第三个传输参数:format_name指的是复合流的格式,比方说:flv、ts、mp4等等
  • 第四个传输参数:filename是输出地址,输出地址可以是本地文件(如:xxx.mp4、xxx.ts等等)。也可以是网络流地址(如:rtmp://xxx.xxx.xxx.xxx:1935/live/01)

这个API是根据我们流媒体类型去分配AVFormatContext结构体。我们传进来的类型会分为FLV_PROTOCOLTS_PROTOCOL,具体如何配置如下面:

// 协议类型为 FLV_PROTOCOL,则使用 "flv" 封装格式
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv",
                                       ffmpeg_config->network_addr);
ret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", 
                                        ffmpeg_config->network_addr);

注意

  • TS格式分别可以适配以下流媒体复合流,包括:SRT、UDP、TS本地文件等。
  • flv格式包括:RTMP、FLV本地文件等等。

然后就是去avformat_new_stream初始化AVStream结构体,由于流可能不止一种流,所以为了模块化就使用一个函数封装AVStream结构体初始化

// --- add_stream 函数实现 ---
int add_stream(OutputStream*      ost,
               AVFormatContext*   oc,
               enum AVCodecID     codec_id,
               int                width,
               int                height,
               enum AVPixelFormat pix_fmt,
               AVRational         time_base)

在其内部进行AVStream初始化

AVStream * avformat_new_stream(AVFormatContext *s, AVDictionary **options);
  • 第一个传输参数:AVFormatContext的结构体指针
  • 第二个传输参数:AVDictionary结构体指针的指针
  • 返回值:AVStream结构体指针
// 创建输出码流的 AVStream
ost->stream = avformat_new_stream(oc, NULL);

FLV流数据

FLV流媒体协议是美国Adobe公司推出来的一种流媒体协议。FLV流媒体格式的特点是封装过后的音视频数据非常小、并且封装的规范相对更加简单,所以FLV流媒体格式非常适合网络传输。但是FLV格式是Adobe公司的私有协议,所以它支持的网络传输协议比较有限:如RTMP、HTTP-FLV

FLV流媒体封装格式一般由两个部分组成,一个是FLV Header、另外一个是FLV Body。其中,FLV Header长度固定式9个字节FLV BODY则是由一组组的Previous Tags Size + Tag组成。Previous Tag Size一般在整个Tag的前面,它一般记录前一个Tag的大小。Tag的类型一般分为三种、分别是脚本数据帧类型视频数据类型音频数据类型

┌─────────────┬─────────────────────┬─────────────────────┬─────────────────────┬─────────────────────┬─────────────────────┬─────────────┐
│  FLV        │ Previous Tag Size   │ Tag(meta data)      │ PreviousTag Size    │ Tag(video)          │ Previous Tag Size   │ Tag(audio)  │
│  Header     │                     │                     │                     │    H.264            │                     │    AAC      │
├─────────────┼─────────────────────┼─────────────────────┼─────────────────────┼─────────────────────┼─────────────────────┼─────────────┤
│ 9byte       │                     │                     │                     │                     │                     │             │
└─────────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────────────────────┴─────────────┘
       ↑                                   ↑
       │                                   │
  ┌────┴────┐                       ┌──────┴──────┐
  │ 9byte   │                       │    Body     │
  └─────────┘                       └─────────────┘

FLV header

FLVHEADERpng

前三个字节是签名,第四个字节是版本号,Version是版本号固定为1,第五个字节前五个bit是保留为0,第六bit是表示是否有音频,有音频就是1,否则为0;最后一个bit是表示是否有视频,有视频是1,否则为0; 最后四个字节是FLV Header长度

FLV BODY

FLV BODY一般由FLV Tag HeaderTag Data组成。

  • 如果是视频的TAG DATA,则FLV Tag Header+ Video Tag Data
  • 如果是音频的TAG DATA,则是FLV Tag Header + Audio Tag Data
  • 但无论是视频的Tag Data还是音频的Tag Data它们的FLV Tag Header都是相同的

FLV Tag Header

FLVTAGHEADERpng

FLV Script Tag

FLV Script Tag也是由FLV Tag Header + Script Data Tag组成。Script Tag的类型一般被称为MedtaData Tag,它一般会存储一些关于FLV音视频的参数信息,比方说:分辨率(width、height)、duration,通常来说Script Tag Data是第一个出现的Tag,并且有且只有一个。

Script Tag是由两个AMF包组合起来(AMF 包 = 数据类型 + 数据长度 + 数据)。AMF1的第一个字节表示包类型、默认0x02。第2-3个字节代表的是字符串的长度,默认0X00A。 而后面的字节是具体的字符串(“onMetaData”)用十六进制表示:(6f、6e、4d、65、74、61、44、61、74、61)。

而第二个AMF包,第一个字节是0x08表示数组类型。第2-5个字节表示的是数组元素的个数、而后面的数组则是每个数组的键值对。

FLV Script Tag大致结构如下:

┌──────────────────────────────────────────────────────────────────────────────┐
│ FLV Tag Structure (Script/Metadata Tag)                                      │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  ┌─────────────────────────────────────────────────────────────────────┐  │
│  │ FLV Tag Header (11 bytes)                                           │  │
│  ├──────────┬──────────────────┬──────────────────┬────────────────────┤  │
│  │ Byte 0   │ Bytes 1-3        │ Bytes 4-7        │ Bytes 8-10         │  │
│  ├──────────┼──────────────────┼──────────────────┼────────────────────┤  │
│  │ TagType  │ DataSize         │ Timestamp        │ StreamID           │  │
│  │ 0x08     │ (24-bit)         │ (32-bit)         │ 0x000000           │  │
│  └──────────┴──────────────────┴──────────────────┴────────────────────┘  │
│                                                                              │
│  ┌──────────────────────────────────────────────────────────────────────┐  │
│  │ Tag Data (ScriptData = AMF0 编码)                                     │  │
│  │                                                                      │  │
│  │  ┌─────────────────────────────────────────────────────────────┐  │  │
│  │  │ AMF Packet #1: Command Name                                 │  │  │
│  │  ├──────────┬──────────────┬───────────────────────────────────┤  │  │
│  │  │ Type     │ Length       │ String Data                       │  │  │
│  │  │ 0x02     │ 0x00 0x0A    │ "onMetaData"                      │  │  │
│  │  │ (String) │ (10 bytes)   │ 6F 6E 4D 65 74 61 44 61 74 61     │  │  │
│  │  └──────────┴──────────────┴───────────────────────────────────┘  │  │
│  │                                                                      │  │
│  │  ┌─────────────────────────────────────────────────────────────┐  │  │
│  │  │ AMF Packet #2: ECMA Array (Metadata 键值对)                  │  │  │
│  │  ├──────────┬──────────────┬───────────────────────────────────┤  │  │
│  │  │ Type     │ Count        │ 键值对形式                          │  │  │
│  │  │ 0x08     │ 4-byte       │ [Key][Value]...[0x00 0x00 0x09]   │  │  │
│  │  │ (ECMA    │ (element     │                                   │  │  │
│  │  │  Array)  │  count)      │ Example:                          │  │  │
│  │  │          │              │   ├─ Key: "duration"              │  │  │
│  │  │          │              │   │  Len: 0x0008                  │  │  │
│  │  │          │              │   │  Str: 64 75 72 61 74 69 6F 6E │  │  │
│  │  │          │              │   └─ Value: 0x00 (Number)         │  │  │
│  │  │          │              │          + 8-byte double          │  │  │
│  │  │          │              │   ├─ Key: "width"                 │  │  │
│  │  │          │              │   └─ Value: 0x00 + double...      │  │  │
│  │  │          │              │   └─ ...                          │  │  │
│  │  │          │              │   └─ End Marker: 0x00 0x00 0x09   │  │  │
│  │  └──────────┴──────────────┴───────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘

AMF包数据类型定义如下:

图片1png

AMF2对应键值对如下:

图片2png

FLV VIDEO TAG

FLV VIDEO TAG是由两部分组成,FLV Tag HEADER + VIDEO DATA TAG

图片3png

从上图可以看出,VIDEO DATA TAG是视频的具体信息,这其中包括:

  • STREAMID:视频流ID
  • FrameType:视频帧类型(1: avc keyframe指的是关键帧 、2:avc inter frame指的是普通帧)
  • CODECID:编码ID(默认7:AVC编码)
  • AVCPacketType:编码包类型(0: avc sequence hdr、1: NALU类型)
  • CompositionTime:构造时间
  • Data:具体的视频数据

FLV AUDIO TAG

FLV AUDIO TAG是由两部分组成,FLV Tag HEADER + VIDEO DATA TAG

图片4png

从这张图可以看出,AUDIO DATA TAG是视频的具体信息,这其中包括:

  • STREAMID:音频流ID
  • SoundFormat:音频类型(10: aac)
  • SoundRate:音频采样率
  • SoundSize:音频采样深度
  • SoundType:音频编码类型
  • AACPacketType: AAC包的类型
  • Data就是具体的音频数据

每一个Audio Data Tag的具体定义:

图片5png

RTMP协议

RTMP协议是实时消息传输协议的缩写(Real Time Messaging Protocol)的缩写。RTMP协议是基于TCP协议开发的消息传输协议,它是Adobe公司开发的一种私有传输协议。它主要的用途是音视频推流、实时交互语音和数据交互等功能。

RTMP的交互流程

RTMP交互的过程总共可以分为以下几步:握手、建立网络对话、创建网络流、播放

  • 握手,RTMP的握手主要是TCP和RTMP协议之间的握手,它是整个交互过程的开始。
  • 建立网络对话,当握手成功之后,就要建立起Client和Server端的连同关系。
  • 建立网络流,就是当网络会话建立成功后,建立起发送多媒体数据的通道。
  • 播放,指的是Client和Server传输音视频数据的过程。

握手

一个RTMP的开始都是以握手开始的,握手的过程分别为以下步骤:

  1. Client端发送C0(版本号),C1(随机字符串)到Server,而Server收到C0或者C1后发送应答S0,S1
  2. 当Client收到Server端发来的应答数据S0(版本号),S1(随机字符串)后,则发送C2数据到Server。若此时Server同时收到C0、C1后,就开始发送S2到Client
  3. 当Client端和Server端分别收到S2和C2信号后,则代表握手完成。

虽然 RTMP 协议未对这 6 个 Message 的传输顺序做明确规定,但对于 RTMP 协议的实现者而言,必须遵循以下规则:

  • 客户端规则:仅当接收到服务器发来的 S1 后,才可以发送 C2 ;并且只有在收到 S2 之后,才能发送诸如控制信息、真实音视频等其他数据。
  • 服务器规则:只有在接收到客户端发送的 C0 后,才能够发送 S1 ;必须在收到 C1 之后,才可以发送 S2 ;且只有在接收到 C2 后,才能发送控制信息、真实音视频等其他数据。
┌──────────────┐                              ┌──────────────┐
│   Client     │                              │   Server     │
└──────┬───────┘                              └──────┬───────┘
       │                                         │
       │  ┌─────────────────────────────────┐    │
       │  │   Handshake Phase #1 (C0 + C1)  │    │
       │  └─────────────────────────────────┘    │
       │────────────────── C0 ──────────────────>│  C0: 1 byte (version = 0x03)
       │                                         │
       │────────────────── C1 ──────────────────>│  C1: 1536 bytes
       │                                         │      ├─ Timestamp (4 bytes)
       │                                         │      └─ Random data (1532 bytes)
       │                                         │
       │  ┌─────────────────────────────────┐    │
       │  │   Handshake Phase #2 (S0 + S1)  │    │
       │  └─────────────────────────────────┘    │
       │<────────────────── S0 ──────────────────│  S0: 1 byte (version = 0x03)
       │                                         │
       │<────────────────── S1 ──────────────────│  S1: 1536 bytes
       │                                         │      ├─ Timestamp (4 bytes)
       │                                         │      └─ Random data (1532 bytes)
       │                                         │
       │  ┌─────────────────────────────────┐    │
       │  │   Handshake Phase #3 (C2 + S2)  │    │
       │  └─────────────────────────────────┘    │
       │────────────────── C2 ──────────────────>│  C2: 1536 bytes
       │                                         │      ├─ Echo S1.timestamp (4 bytes)
       │                                         │      └─ Echo S1.random (1532 bytes)
       │                                         │
       │<────────────────── S2 ──────────────────│  S2: 1536 bytes
       │                                         │      ├─ Echo C1.timestamp (4 bytes)
       │                                         │      └─ Echo C1.random (1532 bytes)
       │                                         │
       │  ┌─────────────────────────────────┐    │
       │  │   Handshake Complete!           │    │
       │  │   → RTMP Chunk Stream Begins    │    │
       │  └─────────────────────────────────┘    │
       │                                         │

建立网络会话

  1. Client端发送Connect指令到Server端,这个指令的用处就是请求和Server端建立一个连接
  2. Server端接收到Client端发送的指令后,接着发送窗口协议到Client端,并同时连接到应用程序
  3. Server端发送设置带宽的协议到Client端
  4. Client端处理带宽协议后,发送确认窗口大小协议到Server端
  5. Server端发送用户控制信息指令“Stream Begin”到Client端
  6. Server端发送用户消息指令”_result”通知Client端连接状态
Client                           Server
   │  [Handshake Complete]         │
   │─── [1] Connect Command ──────>│
   │<── [2] Connect Result ────────│
   │─── [3] Create Stream ────────>│
   │<── [4] Stream ID (1) ─────────│
   │─── [5] Play "stream001" ─────>│
   │<── [6] Play Started ──────────│
   │<── [7] onMetaData ────────────│
   │<── [8] Video/Audio Data ──────│
   │        [Streaming Active]     │

建立网络流

  1. Client端发送指令”createStream”到Server端
  2. Server端接收到”createStream”指令后,则成功创建网络流。Server并同时发送”__result”指令通知Client端.
┌──────────────┐                          ┌──────────────┐
│   Client     │                          │   Server     │
└──────┬───────┘                          └──────┬───────┘
       │                                         │
       │  ┌─────────────────────────────────┐    │
       │  │   Step 1: Create Stream         │    │
       │  └─────────────────────────────────┘    │
       │                                         │
       │─── [1] createStream ─────────────────>  │  Message Type: 0x14 (Command)
       │        │                                │  Chunk Stream ID: 0x03
       │        ├─ Transaction ID: 2             │  Body (AMF0):
       │        └─ Command Object: null          │    ├─ "createStream"
       │                                         │    ├─ 2.0 (Transaction ID)
       │                                         │    └─ null
       │                                         │
       │<── [2] _result ───────────────────────  │  Message Type: 0x14
       │        │                                │  Body (AMF0):
       │        ├─ Transaction ID: 2             │    ├─ "_result"
       │        └─ Stream ID: 1                  │    ├─ 2.0
       │                                         │    └─ 1.0 (Stream ID = 1)
       │                                         │

播放流

  1. Client端发送指令”play”到Server端。
  2. Server端接收后,Server端发送ChunkSize协议消息到Client端
  3. Server端发送控制指令”streambegin”给Client端
  4. 若第三步发送成功之后,Server端会发送”响应状态” NetStream.Play.Start & NetStream.Play.reset通知客户端“Play”指令成功。

RTMP消息协议

RTMP的消息协议一般分为三种,分别是消息(Message)消息块(Chunk)消息分块(Msg)

消息(Messgae)

Message是RTMP传输协议的最基本单元。在Messgae中,不同类型的Messgae含有不同的Message Type Id。在RTMP协议中,一共有十几种Message Type

比方说Message Type ID区间在1-6则代表此消息协议是控制协议。Message Type ID为8代表的是此消息传输音频数据,若Message Type ID 为9则代表的是此消息传输的是视频数据。Message Type ID区间在15到20则用于发送AMF编码指令(AMF指的是Flash和服务端常见的编码方式),比方说播放、暂停、Client和Server端进行交互等。

RTMP的Message的首部信息(Rtmp Header),Rtmp Header是由三元组组成,包括:timestamp delta、mesage_length、type_id

aaapng
  • timestamp delta:和上一个chunk的时间差,若这个值大于等于16777215,该字段必须等于16777215
  • timestamp:时间戳,每一个Messgae生成的时间戳。
  • BodySize:指的是message的长度
  • Typeid:指的是message 类型

下面的表格是Message类型的分类:

图片2png

消息块(Chunk)

由于在网络传输中,若网络环境不好的情况下,一个消息单元需要拆分成数据量更小的数据块才能够适应网络的传输。在RTMP协议中规定,每个消息都需要被拆分才能够正常传输。拆分的消息分块(Chunk)由Chunk Header 和 Chunk Data组成。Chunk Header由三部分组成:Chunk Basic Header(块基本头)、Chunk Message Header(块消息头)、Extended TimeStamp(扩展时间戳)

图片3png

Chunk Basic Header:这个字段包含流ID(chunk stream id)和分块类型(fmt),流id一般用csid来表示,chunk type决定了Message Header的格式,Chunk Type长度固定为2bit。若Basic Header长度等于1Byte,则cs id取值范围在[0,64]。

而CSID: 0, 1是由协议保存的特殊协议:0代表的是整个Chunk Basic Header要占用两个字节,CSID的取值范围是[64,319]; 1代表的还是占用三个字节,CSID取值范围[64,65599]。

消息分块(Msg)

在网络传输中,每一个RTMP协议单元都需要被分割成一块一块内容才能够发送。其中Message Body指的是每一个分割消息的负载部分,而每个被分割的数据块大小固定为128个字节,并且在首部加上Chunk Header就组成了一块完整的消息分块。一个大小为307个字节的消息分块,被分割成128个字节进行传输。

图片4png

时间基&时间戳

时间基

时间基也称之为时间基准,它代表的是每个刻度是多少秒。比方说:视频帧率是25FPS,那它的时间刻度是{1,25}。相当于1s内划分出25个等分,也就是每隔1/25秒后显示一帧视频数据。

图片1png

在FFMPEG中时间基准都是用AVRational结构体来表示:

图片2png

num:它是numerator的缩写,代表的是分子 den:它是denominator的缩写,代表的是分母

  • 视频时间基都是以帧率为单位,比方说25帧。FFMPEG就以AVRational video_timebase = {1, 25}来表示。
  • 音频时间基都是以采样率为单位,比方说音频采样率是48000HZ。FFMPEG就以AVRational audio_timebase = {1, 48000}来表示。

对于封装格式来说:flv 封装格式的 time_base 为{1,1000},ts 封装格式的 time_base 为{1,90000}

图片3png

从上图ffplay的信息我们可以看到有很多关于时间基的信息:

  • tbr:表示帧率,该帧率是一个基准,通常来说tbr和fps是一致的
  • tbn:表示视频流timebase(时间基),比方说:TS格式的数据timebase是90000,flv格式的视频流timebase为1000
  • tbc:表示视频流codec timebase这个值一般为帧率的两倍。比方说:帧率是25fps,则tbc是50

时间戳

首先时间戳它指的是在时间轴里面占了多少个格子,时间戳的单位不是具体的秒数,而是时间刻度。只有当时间基和时间戳结合在一起的时候,才能够真正表达出来时间是多少

PTS

PTS:全称是Presentation Time Stamp(显示时间戳),它主要的作用是度量解码后的视频帧什么时候显示出来。

视频PTS计算:n为第n帧视频帧,timebase是{1,framerate},fps是framerate

PTS(n) = n × (framerate / framerate) = n

举例子:n = 1, pts = 1 n = 2, pts = 2 n =3, pts = 3

音频PTS计算:n为第n帧音频帧,nb_samples指的是采样个数(AAC默认1024),timebase是{1,samplerate},samplerate是采样率

一帧音频的持续时间(time_base单位)= nb_samples

PTS(n) = n × nb_samples

例如如:Samplerate = 48000, nb_sample=1024, timebase = {1,48000}

        n = 1, pts = 1024 n = 2, pts = 2048 n = 3, pts = 3072

DTS

表示的是压缩解码的时间戳,在没有B帧的情况下PTS 等于 DTS。假设编码的里面引入了B帧,则还要计算B帧的时间。

  • 没有B帧:dts = pts
  • 存在B帧:dts = pts + b_time

时间转换

在FFMPEG中由于不同的复合流,时间基是不同的,比方说:

flv的时间基time_base= {1,1000},假设一个视频time_base = {1,25},我们需要合成mpegts文件,它就需要把time_base = {1,25}占的格子转换成time_base = {1,1000}占的格子

在FFMPEG中用以下的API进行时间基转换:

图片4png
void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);
  • 第一个参数:AVPacket结构体指针
  • 第二个参数:源时间基
  • 第三个参数:目的时间基

上面这个api的用法是,把AVPacket的时间基tb_src转换成时间基tb_dst

推流过程

初始化RKMEDIA_FFMPEG_CONFIG结构体

// 为 RKMEDIA_FFMPEG_CONFIG 结构体分配内存
    RKMEDIA_FFMPEG_CONFIG* ffmpeg_config =
        (RKMEDIA_FFMPEG_CONFIG*)malloc(sizeof(RKMEDIA_FFMPEG_CONFIG);
// 关键:在使用前将整个结构体内存清零,确保所有指针和成员都是初始化状态
    memset(ffmpeg_config, 0, sizeof(RKMEDIA_FFMPEG_CONFIG));

    // 设置基本配置参数
    ffmpeg_config->width         = WIDTH;
    ffmpeg_config->height        = HEIGHT;
    ffmpeg_config->config_id     = 0;             // 可以根据需要设置
    ffmpeg_config->protocol_type = FLV_PROTOCOL;  // 默认为 RTMP (FLV) 协议
    ffmpeg_config->video_codec = AV_CODEC_ID_H264;  // 指定视频编码器为 H.264
    ffmpeg_config->audio_codec =
        AV_CODEC_ID_NONE;  // 指定音频编码器为 AAC (如果不需要音频,设为
                           // AV_CODEC_ID_NONE)

调用init_rkmedia_ffmpeg_context来初始化

// 调用核心的 FFmpeg 上下文初始化函数
    int ret = init_rkmedia_ffmpeg_context(ffmpeg_config);
    if (ret != 0) {
        fprintf(stderr,
                "init_ffmpeg_config: init_rkmedia_ffmpeg_context failed with "
                "error code: %d\n",
                ret);
        // init_rkmedia_ffmpeg_context 内部已处理了大部分失败清理
        free(ffmpeg_config);  // 释放主结构体内存
        *config_out = NULL;
        return -1;  // 返回错误码
    }

在这个函数内部进行AVFormatContext等一系列初始化

获取一帧数据

使用自定义函数 deal_video_avpacket 函数 (处理视频 AVPacket 并推流)

首先从视频队列获取一帧数据

std::unique_ptr<video_data_packet_t> video_data_packet =
        video_queue->getVideoPacketQueue();
    if (video_data_packet == nullptr) {
        // printf("deal_video_avpacket: 视频队列为空,等待数据...\n");
        return -1;  // 表示当前没有数据
    }

然后在获取到数据之后再去初始化网络,用来在关键视频帧里面找PPS/SPS,并且调用avformat_write_header写入流媒体头

传递数据码流到AVPacket

// 清空 AVPacket,准备填充新数据
    av_packet_unref(pkt_out);

    int frame_size = video_data_packet->buffer.size();

    ret = av_grow_packet(pkt_out, frame_size);
    if (ret < 0) {
        std::cerr << "deal_video_avpacket: 无法扩展 AVPacket 大小: "
                  << av_err2str_custom(ret) << std::endl;
        return ret;
    }

    memcpy(pkt_out->data, video_data_packet->buffer.data(), frame_size);
    pkt_out->size = frame_size;

    //AVPacket默认没有设置关键帧,需要手动设置,如果没有关键帧会黑屏
    if (video_data_packet->is_key_frame) {
        pkt_out->flags |= AV_PKT_FLAG_KEY;
    }
    else {
        pkt_out->flags &= ~AV_PKT_FLAG_KEY;
    }
  • av_grow_packet 会自动处理 buf
    • 它内部调用 av_buffer_realloc 重新分配缓冲区。
    • 自动更新 pkt_out->bufpkt_out->data,确保 data 指向 buf->data
    • 释放时通过 av_packet_unref 会正确释放 buf 管理的内存。
  • 直接操作 pkt_out->data 是安全的:因为 av_grow_packet 已确保 data 有效且由 buf 管理。

每一帧AVPacket计算PTS时间戳

// 计算并设置 PTS 和 DTS
    pkt_out->pts = av_rescale_q(video_data_packet->timestamp, AV_TIME_BASE_Q,
                                ost->stream->time_base);
    pkt_out->dts = pkt_out->pts;
    pkt_out->stream_index = ost->stream->index;  
    // 将 AVPacket 的时间戳从其内部时间基(如果已设置)调整到目标流的时间基
    // 如果 pkt->pts/dts 已经是在 ost->stream->time_base
    // 下计算的,这一步可以省略 但为了健壮性,通常保留。这里将 pkt
    // 的时间基调整为流的时间基,不会改变其值。
    av_packet_rescale_ts(pkt, st->time_base, st->time_base);
    pkt->stream_index = st->index;  // 设置数据包所属的流索引
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq);

将时间戳从一个时间基(time_base)转换到另一个时间基

数学本质:
result = a × bq / cq
(其中 bqcq 是有理数 AVRational{num, den},即 num/den

把每一帧视频数据传输到流媒体服务器

时间基转换完成之后,就把视频数据写入到复合流文件里面,调用的API是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。

// 写入数据包到复合流
    int ret = av_interleaved_write_frame(fmt_ctx, pkt);
    if (ret < 0) {
        fprintf(stderr, "write_ffmpeg_avpacket: 写入数据包到复合流失败: %s\n",
                av_err2str_custom(ret));
    }
    return ret;
上一篇
下一篇