FFmpeg FLV解码器原理深度解析

FFmpeg FLV解码器原理深度解析:从格式解析到数据提取的完整流程

目录

  1. 引言
  2. FLV解封装概述
  3. [FFmpeg FLV解码器架构](#FFmpeg FLV解码器架构)
  4. 核心数据结构解析
  5. 解封装流程详解
  6. 关键函数深度剖析
  7. 元数据解析机制
  8. 关键帧索引解析
  9. [Enhanced RTMP支持](#Enhanced RTMP支持)
  10. 多轨道解封装
  11. 错误处理与容错机制
  12. 性能优化与最佳实践
  13. 总结

引言

FLV(Flash Video)解封装器是FFmpeg中用于解析FLV格式文件的关键组件。与编码器将音视频数据打包成FLV格式相反,解码器(准确说是解封装器/demuxer)负责从FLV文件中提取音视频流、元数据和时间戳信息,为后续的解码播放做准备。

本文将深入剖析FFmpeg中FLV解封装器(flvdec.c)的实现原理,通过详细的代码分析和流程图,帮助读者全面理解FLV解封装的完整流程。文章将涵盖格式探测、头部解析、数据包读取、元数据处理、关键帧索引构建等各个环节,并特别关注Enhanced RTMP和多轨道支持等高级特性。


FLV解封装概述

2.1 解封装与解码的区别

在深入讲解之前,需要明确两个概念:

解封装(Demuxing)

  • 从容器格式中分离出音频流、视频流和元数据
  • 处理时间戳、同步信息
  • 不涉及压缩数据的解码
  • FLV解封装器属于这一类

解码(Decoding)

  • 将压缩的音视频数据还原为原始数据
  • 例如:H.264 → YUV,AAC → PCM
  • 在解封装之后进行

2.2 FLV解封装的主要任务

复制代码
┌─────────────────────────────────────────┐
│           FLV文件输入                    │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│         1. 格式探测 (Probe)              │
│    - 检测文件签名 "FLV"                  │
│    - 验证版本号                          │
│    - 判断是否为直播流                     │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│         2. 头部解析 (Read Header)        │
│    - 读取FLV文件头                       │
│    - 解析音视频标志                       │
│    - 初始化解封装上下文                   │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│         3. 数据包读取 (Read Packet)      │
│    - 循环读取FLV Tag                     │
│    - 解析Tag类型(音频/视频/脚本)        │
│    - 提取时间戳信息                       │
│    - 解析编解码器信息                     │
│    - 处理extradata                       │
│    - 构建AVPacket                        │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│         4. 元数据处理                     │
│    - 解析onMetaData                      │
│    - 提取duration、width、height等       │
│    - 解析关键帧索引                       │
│    - 处理colorInfo(HDR信息)            │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│         5. 流同步与Seek                   │
│    - 构建索引表                          │
│    - 处理时间戳跳变                       │
│    - 支持快速定位                         │
└─────────────────┬───────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────┐
│         输出:AVPacket流                 │
│    - 视频AVPacket                        │
│    - 音频AVPacket                        │
│    - 字幕AVPacket                        │
└─────────────────────────────────────────┘

FFmpeg FLV解码器架构

3.1 模块组织结构

FLV解封装器实现了FFmpeg的AVInputFormat接口:

复制代码
AVFormatContext
    │
    ├── AVInputFormat (ff_flv_demuxer)
    │       │
    │       ├── read_probe: flv_probe
    │       ├── read_header: flv_read_header
    │       ├── read_packet: flv_read_packet
    │       ├── read_seek: flv_read_seek
    │       └── read_close: flv_read_close
    │
    └── FLVContext (私有数据)
            │
            ├── 配置选项
            │   ├── trust_metadata
            │   ├── trust_datasize
            │   └── dump_full_metadata
            │
            ├── 流状态
            │   ├── last_sample_rate
            │   ├── last_channels
            │   └── wrong_dts
            │
            ├── 关键帧索引
            │   ├── keyframe_times
            │   ├── keyframe_filepositions
            │   └── keyframe_count
            │
            ├── 元数据缓存
            │   ├── new_extradata
            │   ├── mt_extradata (多轨道)
            │   └── meta_color_info
            │
            └── 同步信息
                ├── time_offset
                ├── time_pos
                └── validate_index

3.2 解封装器生命周期

复制代码
开始
  │
  ├─→ flv_probe()           [格式探测阶段]
  │   ├─ 检查文件签名
  │   ├─ 验证版本号
  │   └─ 返回匹配分数
  │
  ├─→ flv_read_header()     [头部读取阶段]
  │   ├─ 跳过文件签名
  │   ├─ 读取标志位
  │   ├─ 读取头部大小
  │   └─ 初始化上下文
  │
  ├─→ flv_read_packet()     [数据包读取阶段]
  │   ├─ 循环读取Tag
  │   │   ├─ 读取Tag Header
  │   │   ├─ 解析Tag类型
  │   │   ├─ 处理音频Tag
  │   │   ├─ 处理视频Tag
  │   │   ├─ 处理脚本Tag
  │   │   └─ 构建AVPacket
  │   │
  │   └─ 返回AVPacket
  │
  ├─→ flv_read_seek()       [定位阶段]
  │   └─ 使用索引快速定位
  │
  └─→ flv_read_close()      [清理阶段]
      └─ 释放资源

核心数据结构解析

4.1 FLVContext结构体

FLVContext是FLV解封装器的核心数据结构:

c 复制代码
typedef struct FLVContext {
    const AVClass *class;           // AVClass用于选项系统
    
    // 配置选项
    int trust_metadata;              // 是否信任onMetaData中的流信息
    int trust_datasize;              // 是否信任Tag的DataSize字段
    int dump_full_metadata;          // 是否导出完整元数据
    int wrong_dts;                   // DTS是否可能错误(负CTS)
    
    // extradata缓存
    uint8_t *new_extradata[FLV_STREAM_TYPE_NB];     // 新的extradata
    int new_extradata_size[FLV_STREAM_TYPE_NB];     // extradata大小
    
    // 音频流状态
    int last_sample_rate;            // 上一个采样率
    int last_channels;               // 上一个声道数
    
    // 索引验证
    struct {
        int64_t dts;                 // 验证点的DTS
        int64_t pos;                 // 验证点的文件位置
    } validate_index[2];
    int validate_next;               // 下一个验证点索引
    int validate_count;              // 验证点数量
    int searched_for_end;            // 是否已搜索文件末尾
    
    // 重同步缓冲区
    uint8_t resync_buffer[2*RESYNC_BUFFER_SIZE];
    
    // 错误处理
    int broken_sizes;                // 某些编码器的Tag大小错误
    int64_t sum_flv_tag_size;        // Tag大小累加和
    
    // 关键帧索引
    int last_keyframe_stream_index;  // 最后一个关键帧的流索引
    int keyframe_count;              // 关键帧数量
    int64_t video_bit_rate;          // 视频码率
    int64_t audio_bit_rate;          // 音频码率
    int64_t *keyframe_times;         // 关键帧时间戳数组
    int64_t *keyframe_filepositions; // 关键帧文件位置数组
    AVRational framerate;            // 帧率
    
    // 时间戳处理
    int64_t last_ts;                 // 最后一个时间戳
    int64_t time_offset;             // 时间偏移(用于拼接文件)
    int64_t time_pos;                // 时间偏移位置
    
    // HDR元数据
    FLVMetaVideoColor meta_color_info;
    enum FLVMetaColorInfoFlag meta_color_info_flag;
    
    // 多轨道支持
    uint8_t **mt_extradata;          // 多轨道extradata数组
    int *mt_extradata_sz;            // 多轨道extradata大小数组
    int mt_extradata_cnt;            // 多轨道数量
} FLVContext;

4.2 FLVMetaVideoColor结构体

用于存储HDR视频的颜色信息:

c 复制代码
typedef struct FLVMetaVideoColor {
    enum AVColorSpace matrix_coefficients;           // 矩阵系数
    enum AVColorTransferCharacteristic trc;          // 传输特性
    enum AVColorPrimaries primaries;                 // 色域
    uint16_t max_cll;                                // 最大内容亮度
    uint16_t max_fall;                               // 最大帧平均亮度
    FLVMasteringMeta mastering_meta;                 // 母版显示元数据
} FLVMetaVideoColor;

typedef struct FLVMasteringMeta {
    float r_x, r_y;                  // 红色主色点坐标
    float g_x, g_y;                  // 绿色主色点坐标
    float b_x, b_y;                  // 蓝色主色点坐标
    float white_x, white_y;          // 白点坐标
    float max_luminance;             // 最大亮度
    float min_luminance;             // 最小亮度
} FLVMasteringMeta;

解封装流程详解

5.1 格式探测阶段 (flv_probe)

格式探测是解封装的第一步,用于判断文件是否为FLV格式。

5.1.1 探测流程
复制代码
flv_probe(AVProbeData *p)
    │
    ├─→ 检查文件签名
    │   ├─ p->buf[0] == 'F'
    │   ├─ p->buf[1] == 'L'
    │   └─ p->buf[2] == 'V'
    │
    ├─→ 检查版本号
    │   └─ p->buf[3] < 5
    │
    ├─→ 检查保留字节
    │   └─ p->buf[5] == 0
    │
    ├─→ 验证头部偏移
    │   ├─ offset = AV_RB32(p->buf + 5)
    │   ├─ offset > 8
    │   └─ offset + 100 < p->buf_size
    │
    ├─→ 检测直播流标识(可选)
    │   └─ 检查 "NGINX RTMP" 标识
    │
    └─→ 返回匹配分数
        └─ AVPROBE_SCORE_MAX (100)
5.1.2 关键代码分析
c 复制代码
static int flv_probe(const AVProbeData *p)
{
    const uint8_t *d = p->buf;
    unsigned offset = AV_RB32(d + 5);

    // 检查FLV签名和基本格式
    if (d[0] == 'F' &&
        d[1] == 'L' &&
        d[2] == 'V' &&
        d[3] < 5 &&        // 版本号 < 5
        d[5] == 0 &&       // 保留字节
        offset + 100 < p->buf_size &&
        offset > 8) {
        
        // 检测是否为NGINX RTMP直播流
        int is_live = !memcmp(d + offset + 40, "NGINX RTMP", 10);
        
        // live参数为0表示普通FLV文件
        if (live == is_live)
            return AVPROBE_SCORE_MAX;
    }
    return 0;
}

探测要点

  1. 文件签名:前3字节必须是"FLV"
  2. 版本号:第4字节,当前版本为1,小于5都可接受
  3. 保留字节:第6字节必须为0
  4. 头部偏移:通常为9,但需要验证合理性
  5. 直播流检测:通过特定标识区分直播流和点播文件

5.2 头部读取阶段 (flv_read_header)

头部读取阶段初始化解封装上下文,读取FLV文件头。

5.2.1 FLV文件头结构
复制代码
FLV File Header (9 bytes):
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│  'F'   │  'L'   │  'V'   │Version │  Flags │    HeaderSize (4 bytes)           │
│ 0x46   │ 0x4C   │ 0x56   │  0x01  │  0x05  │         0x00000009                │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
   0        1        2        3        4        5        6        7        8

Flags字节 (byte 4):
    Bit 0: Reserved (0)
    Bit 1: Reserved (0)
    Bit 2: HasAudio (1 = 有音频)
    Bit 3: Reserved (0)
    Bit 4: HasVideo (1 = 有视频)
    Bit 5-7: Reserved (0)
5.2.2 函数执行流程
复制代码
flv_read_header()
    │
    ├─→ 处理KUX格式(优酷加密格式)
    │   └─ 跳过0xe40000字节
    │
    ├─→ 跳过文件签名
    │   └─ avio_skip(pb, 4)  // 跳过"FLV"+版本号
    │
    ├─→ 读取标志位
    │   ├─ flags = avio_r8(pb)
    │   └─ 提取HasAudio和HasVideo标志
    │
    ├─→ 读取头部大小
    │   └─ offset = avio_rb32(pb)
    │
    ├─→ 定位到数据区
    │   └─ avio_seek(pb, offset, SEEK_SET)
    │
    ├─→ 读取PreviousTagSize0
    │   ├─ pre_tag_size = avio_rb32(pb)
    │   └─ 验证是否为0(标准要求)
    │
    └─→ 初始化上下文
        ├─ s->start_time = 0
        ├─ flv->sum_flv_tag_size = 0
        └─ flv->last_keyframe_stream_index = -1
5.2.3 关键代码分析
c 复制代码
static int flv_read_header(AVFormatContext *s)
{
    FFFormatContext *const si = ffformatcontext(s);
    int flags;
    FLVContext *flv = s->priv_data;
    int offset;
    int pre_tag_size = 0;

    // 处理KUX格式(优酷加密的FLV)
    if(!strcmp(s->iformat->name, "kux"))
        avio_skip(s->pb, 0xe40000);

    // 跳过"FLV"签名和版本号
    avio_skip(s->pb, 4);
    
    // 读取标志位
    flags = avio_r8(s->pb);
    
    // 记录缺失的流类型(用于后续创建)
    si->missing_streams = flags & (FLV_HEADER_FLAG_HASVIDEO | FLV_HEADER_FLAG_HASAUDIO);
    
    // 设置NOHEADER标志,表示流信息需要从数据中推断
    s->ctx_flags |= AVFMTCTX_NOHEADER;

    // 读取头部大小并定位
    offset = avio_rb32(s->pb);
    avio_seek(s->pb, offset, SEEK_SET);

    // 读取PreviousTagSize0(标准要求为0)
    pre_tag_size = avio_rb32(s->pb);
    if (pre_tag_size) {
        av_log(s, AV_LOG_WARNING, 
               "Read FLV header error, input file is not a standard flv format, "
               "first PreviousTagSize0 always is 0\n");
    }

    // 初始化上下文
    s->start_time = 0;
    flv->sum_flv_tag_size = 0;
    flv->last_keyframe_stream_index = -1;

    return 0;
}

重要概念

  • missing_streams:记录文件头声明有但还未创建的流
  • AVFMTCTX_NOHEADER:表示流信息不完整,需要从数据中推断
  • PreviousTagSize0:第一个PreviousTagSize必须为0

5.3 数据包读取阶段 (flv_read_packet)

这是解封装器最核心的函数,负责从FLV文件中读取Tag并转换为AVPacket。

5.3.1 整体流程图
复制代码
flv_read_packet()
    │
    ├─→ 读取Tag Header (11 bytes)
    │   ├─ TagType (1 byte)
    │   ├─ DataSize (3 bytes)
    │   ├─ Timestamp (4 bytes)
    │   └─ StreamID (3 bytes)
    │
    ├─→ 解析Tag类型
    │   │
    │   ├─→ 音频Tag (0x08)
    │   │   ├─ 读取AudioFlags
    │   │   ├─ 检测Enhanced FLV
    │   │   ├─ 解析编解码器ID
    │   │   ├─ 处理多轨道
    │   │   └─ 设置音频参数
    │   │
    │   ├─→ 视频Tag (0x09)
    │   │   ├─ 读取VideoFlags
    │   │   ├─ 检测Enhanced FLV
    │   │   ├─ 解析编解码器ID
    │   │   ├─ 处理多轨道
    │   │   ├─ 解析CompositionTime
    │   │   └─ 设置视频参数
    │   │
    │   └─→ 脚本Tag (0x12)
    │       ├─ 调用flv_read_metabody()
    │       ├─ 解析onMetaData
    │       ├─ 解析onTextData
    │       └─ 解析onCaption
    │
    ├─→ 查找或创建AVStream
    │   ├─ 遍历现有流
    │   ├─ 匹配编解码器和track_idx
    │   └─ 如果不存在则创建新流
    │
    ├─→ 处理extradata
    │   ├─ 检测Sequence Header
    │   ├─ 读取extradata
    │   └─ 标记需要上下文更新
    │
    ├─→ 读取Tag Data
    │   └─ av_get_packet(pb, pkt, size)
    │
    ├─→ 设置AVPacket属性
    │   ├─ pkt->dts = dts
    │   ├─ pkt->pts = pts
    │   ├─ pkt->stream_index
    │   └─ pkt->flags (关键帧标志)
    │
    ├─→ 添加索引条目
    │   └─ av_add_index_entry()
    │
    ├─→ 读取PreviousTagSize
    │   └─ 验证Tag大小一致性
    │
    └─→ 返回AVPacket
5.3.2 Tag Header解析

每个FLV Tag都以11字节的Header开始:

c 复制代码
// 读取Tag类型(低5位)
type = (avio_r8(s->pb) & 0x1F);

// 读取数据大小(24位大端)
size = avio_rb24(s->pb);

// 读取时间戳(32位,分两部分)
dts  = avio_rb24(s->pb);       // 低24位
dts |= (unsigned)avio_r8(s->pb) << 24;  // 高8位

// 跳过StreamID(总是0)
avio_skip(s->pb, 3);

时间戳格式

  • FLV使用32位时间戳,单位为毫秒
  • 低24位存储在前3字节
  • 高8位存储在第4字节(TimestampExtended)
  • 最大支持约49.7天(2^32毫秒)
5.3.3 音频Tag处理
复制代码
音频Tag处理流程
    │
    ├─→ 读取AudioFlags (1 byte)
    │   ├─ Bit 0: SoundType (0=Mono, 1=Stereo)
    │   ├─ Bit 1: SoundSize (0=8bit, 1=16bit)
    │   ├─ Bit 2-3: SoundRate
    │   └─ Bit 4-7: SoundFormat
    │
    ├─→ 检测Enhanced FLV
    │   └─ if (SoundFormat == 9) // FLV_CODECID_EX_HEADER
    │       ├─ enhanced_flv = 1
    │       └─ 读取扩展头
    │
    ├─→ 处理PacketType
    │   ├─ AudioPacketTypeSequenceStart (0)
    │   │   └─ 读取extradata
    │   │
    │   ├─ AudioPacketTypeCodedFrames (1)
    │   │   └─ 读取音频数据
    │   │
    │   ├─ AudioPacketTypeMultichannelConfig (4)
    │   │   └─ 解析多声道配置
    │   │
    │   └─ AudioPacketTypeMultitrack (5)
    │       └─ 处理多轨道
    │
    ├─→ 解析编解码器
    │   └─ flv_set_audio_codec()
    │
    └─→ 设置音频参数
        ├─ sample_rate
        ├─ channels
        └─ bits_per_coded_sample

Enhanced FLV音频格式

对于Enhanced FLV,音频Tag格式如下:

复制代码
Standard Audio Tag:
┌──────────┬──────────┬──────────────┐
│AudioFlags│PacketType│  Audio Data  │
│ (1 byte) │ (1 byte) │  (N bytes)   │
└──────────┴──────────┴──────────────┘

Enhanced Audio Tag:
┌──────────┬──────────┬──────────────┬──────────┬──────────────┐
│AudioFlags│ FourCC   │  TrackIdx    │PacketType│  Audio Data  │
│ (1 byte) │(4 bytes) │  (1 byte)*   │ (varies) │  (N bytes)   │
└──────────┴──────────┴──────────────┴──────────┴──────────────┘
* TrackIdx仅在多轨道模式下存在
5.3.4 视频Tag处理
复制代码
视频Tag处理流程
    │
    ├─→ 读取VideoFlags (1 byte)
    │   ├─ Bit 0-3: CodecID
    │   ├─ Bit 4-6: FrameType
    │   └─ Bit 7: IsExHeader (Enhanced FLV)
    │
    ├─→ 检测Enhanced FLV
    │   └─ enhanced_flv = (flags >> 7) & 1
    │
    ├─→ 处理PacketType
    │   ├─ PacketTypeSequenceStart (0)
    │   │   └─ 读取编解码器配置
    │   │       ├─ H.264: AVCDecoderConfigurationRecord
    │   │       ├─ HEVC: HEVCDecoderConfigurationRecord
    │   │       ├─ AV1: AV1CodecConfigurationRecord
    │   │       └─ VP9: VPCodecConfigurationRecord
    │   │
    │   ├─ PacketTypeCodedFrames (1)
    │   │   ├─ 读取CompositionTime (3 bytes)
    │   │   └─ 读取视频数据
    │   │
    │   ├─ PacketTypeCodedFramesX (3)
    │   │   └─ 不含CompositionTime(PTS==DTS)
    │   │
    │   ├─ PacketTypeMetadata (4)
    │   │   └─ 解析colorInfo(HDR信息)
    │   │
    │   └─ PacketTypeMultitrack (6)
    │       └─ 处理多轨道
    │
    ├─→ 解析编解码器
    │   └─ flv_set_video_codec()
    │
    ├─→ 计算PTS
    │   └─ pts = dts + CompositionTime
    │
    └─→ 处理关键帧
        └─ if (FrameType == 1) // KeyFrame
            └─ pkt->flags |= AV_PKT_FLAG_KEY

CompositionTime处理

CompositionTime是一个24位有符号整数,表示PTS与DTS的差值:

c 复制代码
// 读取CompositionTime(24位有符号数)
int32_t cts = (avio_rb24(s->pb) + 0xff800000) ^ 0xff800000;

// 计算PTS
pts = av_sat_add64(dts, cts);

// 检测负CTS(可能表示DTS错误)
if (cts < 0) {
    if (!flv->wrong_dts)
        av_log(s, AV_LOG_WARNING,
            "Negative cts, previous timestamps might be wrong.\n");
    flv->wrong_dts = 1;
}

为什么会有负CTS?

  • B帧的PTS可能小于DTS
  • 某些编码器可能产生错误的时间戳
  • 需要特殊处理以避免时间戳混乱
5.3.5 脚本Tag处理

脚本Tag包含元数据信息,最常见的是onMetaData:

c 复制代码
if (type == FLV_TAG_TYPE_META) {
    stream_type = FLV_STREAM_TYPE_SUBTITLE;
    if (size > 13 + 1 + 4) {
        int type;
        meta_pos = avio_tell(s->pb);
        
        // 解析元数据
        type = flv_read_metabody(s, next);
        
        if (type == 0 && dts == 0 || type < 0) {
            goto skip;
        } else if (type == TYPE_ONTEXTDATA) {
            return flv_data_packet(s, pkt, dts, next);
        } else if (type == TYPE_ONCAPTION) {
            return flv_data_packet(s, pkt, dts, next);
        } else if (type == TYPE_UNKNOWN) {
            stream_type = FLV_STREAM_TYPE_DATA;
        }
        
        avio_seek(s->pb, meta_pos, SEEK_SET);
    }
}

关键函数深度剖析

6.1 flv_set_audio_codec函数

该函数根据FLV音频编解码器ID设置AVCodecParameters:

c 复制代码
static void flv_set_audio_codec(AVFormatContext *s, AVStream *astream,
                                AVCodecParameters *apar, int flv_codecid)
{
    switch (flv_codecid) {
    case FLV_CODECID_PCM:
        apar->codec_id = apar->bits_per_coded_sample == 8
                           ? AV_CODEC_ID_PCM_U8
#if HAVE_BIGENDIAN
                           : AV_CODEC_ID_PCM_S16BE;
#else
                           : AV_CODEC_ID_PCM_S16LE;
#endif
        break;
        
    case FLV_CODECID_AAC:
        apar->codec_id = AV_CODEC_ID_AAC;
        break;
        
    case FLV_CODECID_MP3:
        apar->codec_id = AV_CODEC_ID_MP3;
        ffstream(astream)->need_parsing = AVSTREAM_PARSE_FULL;
        break;
        
    case FLV_CODECID_SPEEX:
        apar->codec_id = AV_CODEC_ID_SPEEX;
        apar->sample_rate = 16000;  // FLV Speex固定16kHz
        break;
        
    case FLV_CODECID_NELLYMOSER_8KHZ_MONO:
        apar->sample_rate = 8000;
        apar->codec_id = AV_CODEC_ID_NELLYMOSER;
        break;
        
    // Enhanced FLV编解码器
    case MKBETAG('O', 'p', 'u', 's'):
        apar->codec_id = AV_CODEC_ID_OPUS;
        apar->sample_rate = 48000;  // OPUS固定48kHz
        return;
        
    case MKBETAG('f', 'L', 'a', 'C'):
        apar->codec_id = AV_CODEC_ID_FLAC;
        return;
        
    case MKBETAG('a', 'c', '-', '3'):
        apar->codec_id = AV_CODEC_ID_AC3;
        return;
        
    default:
        avpriv_request_sample(s, "Audio codec (%x)",
               flv_codecid >> FLV_AUDIO_CODECID_OFFSET);
        apar->codec_tag = flv_codecid >> FLV_AUDIO_CODECID_OFFSET;
    }
}

编解码器映射表

FLV CodecID FFmpeg CodecID 说明
0 PCM_U8/PCM_S16LE PCM(根据位深度)
1 ADPCM_SWF ADPCM
2 MP3 MP3
3 PCM_S16LE PCM小端
4/5/6 NELLYMOSER Nellymoser
7 PCM_ALAW A-law PCM
8 PCM_MULAW μ-law PCM
10 AAC AAC
11 SPEEX Speex
'Opus' OPUS OPUS (Enhanced)
'fLaC' FLAC FLAC (Enhanced)
'ac-3' AC3 AC3 (Enhanced)
'ec-3' EAC3 E-AC3 (Enhanced)

6.2 flv_set_video_codec函数

该函数根据FLV视频编解码器ID设置AVCodecParameters:

c 复制代码
static int flv_set_video_codec(AVFormatContext *s, AVStream *vstream,
                               uint32_t flv_codecid, int read)
{
    FFStream *const vstreami = ffstream(vstream);
    int ret = 0;
    AVCodecParameters *par = vstream->codecpar;
    enum AVCodecID old_codec_id = vstream->codecpar->codec_id;

    switch (flv_codecid) {
    case FLV_CODECID_H263:
        par->codec_id = AV_CODEC_ID_FLV1;
        break;
        
    case FLV_CODECID_SCREEN:
        par->codec_id = AV_CODEC_ID_FLASHSV;
        break;
        
    case FLV_CODECID_VP6:
        par->codec_id = AV_CODEC_ID_VP6F;
        // VP6需要读取1字节的extradata
        if (read) {
            if (par->extradata_size != 1) {
                ff_alloc_extradata(par, 1);
            }
            if (par->extradata)
                par->extradata[0] = avio_r8(s->pb);
            else
                avio_skip(s->pb, 1);
        }
        ret = 1;  // 返回1表示消耗了1字节
        break;
        
    case FLV_CODECID_H264:
    case MKBETAG('a', 'v', 'c', '1'):
        par->codec_id = AV_CODEC_ID_H264;
        vstreami->need_parsing = AVSTREAM_PARSE_HEADERS;
        break;
        
    // Enhanced FLV编解码器
    case MKBETAG('h', 'v', 'c', '1'):
        par->codec_id = AV_CODEC_ID_HEVC;
        vstreami->need_parsing = AVSTREAM_PARSE_HEADERS;
        break;
        
    case MKBETAG('a', 'v', '0', '1'):
        par->codec_id = AV_CODEC_ID_AV1;
        vstreami->need_parsing = AVSTREAM_PARSE_HEADERS;
        break;
        
    case MKBETAG('v', 'p', '0', '9'):
        par->codec_id = AV_CODEC_ID_VP9;
        vstreami->need_parsing = AVSTREAM_PARSE_HEADERS;
        break;
        
    default:
        avpriv_request_sample(s, "Video codec (%x)", flv_codecid);
        par->codec_tag = flv_codecid;
    }

    // 检测编解码器中途切换
    if (!vstreami->need_context_update && par->codec_id != old_codec_id) {
        avpriv_request_sample(s, "Changing the codec id midstream");
        return AVERROR_PATCHWELCOME;
    }

    return ret;
}

need_parsing标志

  • AVSTREAM_PARSE_HEADERS:需要解析流头部信息
  • 对于H.264/HEVC/AV1/VP9,需要从数据中提取SPS/PPS等参数

6.3 create_stream函数

该函数创建新的AVStream:

c 复制代码
static AVStream *create_stream(AVFormatContext *s, int codec_type, int track_idx)
{
    FFFormatContext *const si = ffformatcontext(s);
    FLVContext *flv = s->priv_data;
    AVStream *st = avformat_new_stream(s, NULL);
    
    if (!st)
        return NULL;
        
    // 设置流参数
    st->codecpar->codec_type = codec_type;
    st->id = track_idx;  // 用于多轨道
    
    // 设置时间基:32位PTS,单位为毫秒
    avpriv_set_pts_info(st, 32, 1, 1000);
    
    // 如果是第一个轨道(track_idx == 0)
    if (track_idx)
        return st;

    // 检查是否已有足够的流
    if (s->nb_streams >= 3 || 
        (s->nb_streams == 2 && 
         s->streams[0]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE &&
         s->streams[1]->codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE &&
         s->streams[0]->codecpar->codec_type != AVMEDIA_TYPE_DATA &&
         s->streams[1]->codecpar->codec_type != AVMEDIA_TYPE_DATA))
        s->ctx_flags &= ~AVFMTCTX_NOHEADER;
        
    // 设置流特定参数
    if (codec_type == AVMEDIA_TYPE_AUDIO) {
        st->codecpar->bit_rate = flv->audio_bit_rate;
        si->missing_streams &= ~FLV_HEADER_FLAG_HASAUDIO;
    }
    if (codec_type == AVMEDIA_TYPE_VIDEO) {
        st->codecpar->bit_rate = flv->video_bit_rate;
        si->missing_streams &= ~FLV_HEADER_FLAG_HASVIDEO;
        st->avg_frame_rate = flv->framerate;
    }

    // 更新关键帧流索引
    flv->last_keyframe_stream_index = s->nb_streams - 1;
    
    // 添加关键帧索引
    add_keyframes_index(s);
    
    return st;
}

元数据解析机制

7.1 AMF格式解析

FLV使用AMF(Action Message Format)格式存储元数据。

7.1.1 AMF数据类型
c 复制代码
typedef enum {
    AMF_DATA_TYPE_NUMBER      = 0x00,  // 64位双精度浮点数
    AMF_DATA_TYPE_BOOL        = 0x01,  // 布尔值
    AMF_DATA_TYPE_STRING      = 0x02,  // UTF-8字符串
    AMF_DATA_TYPE_OBJECT      = 0x03,  // 对象
    AMF_DATA_TYPE_NULL        = 0x05,  // null
    AMF_DATA_TYPE_UNDEFINED   = 0x06,  // undefined
    AMF_DATA_TYPE_REFERENCE   = 0x07,  // 引用
    AMF_DATA_TYPE_MIXEDARRAY  = 0x08,  // 混合数组
    AMF_DATA_TYPE_OBJECT_END  = 0x09,  // 对象结束
    AMF_DATA_TYPE_ARRAY       = 0x0a,  // 严格数组
    AMF_DATA_TYPE_DATE        = 0x0b,  // 日期
    AMF_DATA_TYPE_LONG_STRING = 0x0c,  // 长字符串
    AMF_DATA_TYPE_UNSUPPORTED = 0x0d,  // 不支持
} AMFDataType;
7.1.2 amf_parse_object函数

这是递归解析AMF对象的核心函数:

复制代码
amf_parse_object(key, depth)
    │
    ├─→ 读取AMF类型标记
    │
    ├─→ 根据类型解析
    │   │
    │   ├─→ AMF_DATA_TYPE_NUMBER
    │   │   └─ num_val = av_int2double(avio_rb64())
    │   │
    │   ├─→ AMF_DATA_TYPE_BOOL
    │   │   └─ num_val = avio_r8()
    │   │
    │   ├─→ AMF_DATA_TYPE_STRING
    │   │   └─ amf_get_string(str_val)
    │   │
    │   ├─→ AMF_DATA_TYPE_OBJECT
    │   │   ├─ 检测"keyframes"对象
    │   │   │   └─ parse_keyframes_index()
    │   │   │
    │   │   └─ 递归解析子对象
    │   │       └─ while (amf_get_string() > 0)
    │   │           └─ amf_parse_object(depth+1)
    │   │
    │   ├─→ AMF_DATA_TYPE_MIXEDARRAY
    │   │   ├─ avio_skip(4)  // 跳过数组大小
    │   │   └─ 递归解析元素
    │   │
    │   ├─→ AMF_DATA_TYPE_ARRAY
    │   │   ├─ arraylen = avio_rb32()
    │   │   └─ 循环解析arraylen个元素
    │   │
    │   └─→ AMF_DATA_TYPE_DATE
    │       ├─ milliseconds = av_int2double(avio_rb64())
    │       └─ timezone = avio_rb16()
    │
    └─→ 处理特定键值
        ├─ "duration" -> s->duration
        ├─ "width" -> vpar->width
        ├─ "height" -> vpar->height
        ├─ "videodatarate" -> video_bit_rate
        ├─ "audiodatarate" -> audio_bit_rate
        ├─ "framerate" -> framerate
        └─ 其他 -> 存入s->metadata
7.1.3 元数据提取示例
c 复制代码
if (depth == 1) {
    if (amf_type == AMF_DATA_TYPE_NUMBER ||
        amf_type == AMF_DATA_TYPE_BOOL) {
        
        // 提取duration
        if (!strcmp(key, "duration"))
            s->duration = num_val * AV_TIME_BASE;
        
        // 提取视频码率
        else if (!strcmp(key, "videodatarate") &&
                 0 <= (int)(num_val * 1024.0))
            flv->video_bit_rate = num_val * 1024.0;
        
        // 提取音频码率
        else if (!strcmp(key, "audiodatarate") &&
                 0 <= (int)(num_val * 1024.0))
            flv->audio_bit_rate = num_val * 1024.0;
        
        // 提取帧率
        else if (!strcmp(key, "framerate")) {
            flv->framerate = av_d2q(num_val, 1000);
            if (vstream)
                vstream->avg_frame_rate = flv->framerate;
        }
        
        // 如果trust_metadata=1,还会提取更多信息
        else if (flv->trust_metadata) {
            if (!strcmp(key, "videocodecid") && vpar) {
                flv_set_video_codec(s, vstream, num_val, 0);
            } else if (!strcmp(key, "audiocodecid") && apar) {
                int id = ((int)num_val) << FLV_AUDIO_CODECID_OFFSET;
                flv_set_audio_codec(s, astream, apar, id);
            } else if (!strcmp(key, "audiosamplerate") && apar) {
                apar->sample_rate = num_val;
            }
            // ... 更多参数
        }
    }
}

7.2 flv_read_metabody函数

该函数解析脚本Tag的内容:

c 复制代码
static int flv_read_metabody(AVFormatContext *s, int64_t next_pos)
{
    FLVContext *flv = s->priv_data;
    AMFDataType type;
    AVStream *stream, *astream, *vstream;
    AVIOContext *ioc;
    char buffer[32];

    astream = NULL;
    vstream = NULL;
    ioc = s->pb;

    // 第一个对象必须是字符串(事件名)
    type = avio_r8(ioc);
    if (type != AMF_DATA_TYPE_STRING ||
        amf_get_string(ioc, buffer, sizeof(buffer)) < 0)
        return TYPE_UNKNOWN;

    // 识别元数据类型
    if (!strcmp(buffer, "onTextData"))
        return TYPE_ONTEXTDATA;
    if (!strcmp(buffer, "onCaption"))
        return TYPE_ONCAPTION;
    if (!strcmp(buffer, "onCaptionInfo"))
        return TYPE_ONCAPTIONINFO;
    if (strcmp(buffer, "onMetaData") && 
        strcmp(buffer, "onCuePoint") && 
        strcmp(buffer, "|RtmpSampleAccess")) {
        av_log(s, AV_LOG_DEBUG, "Unknown type %s\n", buffer);
        return TYPE_UNKNOWN;
    }

    // 查找现有流
    for (i = 0; i < s->nb_streams; i++) {
        stream = s->streams[i];
        if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            vstream = stream;
            flv->last_keyframe_stream_index = i;
        } else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            astream = stream;
            if (flv->last_keyframe_stream_index == -1)
                flv->last_keyframe_stream_index = i;
        }
    }

    // 解析第二个对象(应该是混合数组)
    if (amf_parse_object(s, astream, vstream, buffer, next_pos, 0) < 0)
        return -1;

    return 0;
}

关键帧索引解析

8.1 索引的作用

关键帧索引允许:

  1. 快速Seek:直接跳转到指定时间点
  2. 进度显示:计算播放进度
  3. 缩略图生成:提取关键帧作为缩略图

8.2 parse_keyframes_index函数

该函数从onMetaData中解析关键帧索引:

复制代码
parse_keyframes_index()
    │
    ├─→ 检查是否已解析
    │   └─ if (flv->keyframe_count > 0) return
    │
    ├─→ 循环读取keyframes对象
    │   │
    │   ├─→ 读取键名
    │   │   └─ amf_get_string()
    │   │
    │   ├─→ 验证数据类型
    │   │   └─ if (type != AMF_DATA_TYPE_ARRAY) break
    │   │
    │   ├─→ 读取数组长度
    │   │   └─ arraylen = avio_rb32()
    │   │
    │   ├─→ 识别数组类型
    │   │   ├─ "times" -> keyframe_times
    │   │   └─ "filepositions" -> keyframe_filepositions
    │   │
    │   ├─→ 分配数组内存
    │   │   └─ av_mallocz(sizeof(int64_t) * arraylen)
    │   │
    │   └─→ 读取数组元素
    │       └─ for (i = 0; i < arraylen; i++)
    │           ├─ type = avio_r8()  // 必须是NUMBER
    │           ├─ d = av_int2double(avio_rb64())
    │           └─ current_array[i] = d * factor
    │
    ├─→ 验证索引有效性
    │   ├─ timeslen == fileposlen
    │   ├─ fileposlen > 1
    │   └─ max_pos <= filepositions[0]
    │
    ├─→ 设置验证点
    │   └─ for (i = 0; i < FFMIN(2, fileposlen); i++)
    │       ├─ validate_index[i].pos = filepositions[i]
    │       └─ validate_index[i].dts = times[i]
    │
    └─→ 保存索引
        ├─ flv->keyframe_times = times
        ├─ flv->keyframe_filepositions = filepositions
        └─ flv->keyframe_count = timeslen

8.3 add_keyframes_index函数

该函数将解析的关键帧索引添加到AVStream的索引表:

c 复制代码
static void add_keyframes_index(AVFormatContext *s)
{
    FLVContext *flv = s->priv_data;
    AVStream *stream = NULL;
    unsigned int i = 0;

    // 检查关键帧流是否已创建
    if (flv->last_keyframe_stream_index < 0) {
        av_log(s, AV_LOG_DEBUG, "keyframe stream hasn't been created\n");
        return;
    }

    stream = s->streams[flv->last_keyframe_stream_index];

    // 如果索引表为空,添加所有关键帧
    if (ffstream(stream)->nb_index_entries == 0) {
        for (i = 0; i < flv->keyframe_count; i++) {
            av_log(s, AV_LOG_TRACE, 
                   "keyframe filepositions = %"PRId64" times = %"PRId64"\n",
                   flv->keyframe_filepositions[i], 
                   flv->keyframe_times[i]);
            
            // 添加索引条目
            av_add_index_entry(stream, 
                              flv->keyframe_filepositions[i],  // pos
                              flv->keyframe_times[i],          // timestamp
                              0,                                // size
                              0,                                // min_distance
                              AVINDEX_KEYFRAME);                // flags
        }
    } else {
        av_log(s, AV_LOG_WARNING, "Skipping duplicate index\n");
    }

    // 释放临时数组
    if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
        av_freep(&flv->keyframe_times);
        av_freep(&flv->keyframe_filepositions);
        flv->keyframe_count = 0;
    }
}

8.4 索引验证机制

FLV解封装器使用验证点机制确保索引的准确性:

c 复制代码
if (flv->validate_next < flv->validate_count) {
    int64_t validate_pos = flv->validate_index[flv->validate_next].pos;
    
    if (pos == validate_pos) {
        // 验证时间戳是否匹配
        if (FFABS(dts - flv->validate_index[flv->validate_next].dts) <=
            VALIDATE_INDEX_TS_THRESH) {
            flv->validate_next++;
        } else {
            // 时间戳不匹配,清除索引
            clear_index_entries(s, validate_pos);
            flv->validate_count = 0;
        }
    } else if (pos > validate_pos) {
        // 位置不匹配,清除索引
        clear_index_entries(s, validate_pos);
        flv->validate_count = 0;
    }
}

验证机制的意义

  • 某些FLV文件的关键帧索引可能不准确
  • 通过实际读取验证索引的正确性
  • 如果发现不匹配,清除错误的索引

Enhanced RTMP支持

9.1 Enhanced RTMP概述

Enhanced RTMP是Adobe对RTMP/FLV格式的扩展,支持:

  • 新编解码器(HEVC、AV1、VP9、OPUS、FLAC等)
  • HDR视频(colorInfo元数据)
  • 多轨道音视频
  • 多声道音频

9.2 Enhanced FLV检测

c 复制代码
// 音频Tag
if ((flags & FLV_AUDIO_CODECID_MASK) == FLV_CODECID_EX_HEADER) {
    enhanced_flv = 1;
    pkt_type = flags & ~FLV_AUDIO_CODECID_MASK;
    // 读取FourCC和其他扩展信息
}

// 视频Tag
enhanced_flv = (flags >> 7) & 1;
pkt_type = enhanced_flv ? codec_id : 0;

9.3 colorInfo解析

Enhanced RTMP支持在视频Tag中传输HDR颜色信息:

复制代码
flv_parse_video_color_info()
    │
    ├─→ 验证类型
    │   └─ type == AMF_DATA_TYPE_STRING
    │
    ├─→ 读取字符串
    │   └─ amf_get_string("colorInfo")
    │
    ├─→ 设置解析标志
    │   └─ flv->meta_color_info_flag = FLV_COLOR_INFO_FLAG_PARSING
    │
    ├─→ 解析AMF对象
    │   └─ amf_parse_object()
    │       │
    │       ├─→ colorConfig
    │       │   ├─ transferCharacteristics -> color_trc
    │       │   ├─ matrixCoefficients -> color_space
    │       │   └─ colorPrimaries -> color_primaries
    │       │
    │       ├─→ hdrCll
    │       │   ├─ maxFall -> max_fall
    │       │   └─ maxCLL -> max_cll
    │       │
    │       └─→ hdrMdcv
    │           ├─ redX, redY
    │           ├─ greenX, greenY
    │           ├─ blueX, blueY
    │           ├─ whitePointX, whitePointY
    │           ├─ maxLuminance
    │           └─ minLuminance
    │
    └─→ 标记解析完成
        └─ flv->meta_color_info_flag = FLV_COLOR_INFO_FLAG_GOT

9.4 colorInfo应用

解析的颜色信息会被应用到AVStream:

c 复制代码
static int flv_update_video_color_info(AVFormatContext *s, AVStream *st)
{
    FLVContext *flv = s->priv_data;
    const FLVMetaVideoColor* meta = &flv->meta_color_info;

    // 设置颜色空间参数
    if (meta->matrix_coefficients != AVCOL_SPC_RESERVED)
        st->codecpar->color_space = meta->matrix_coefficients;
    
    if (meta->primaries != AVCOL_PRI_RESERVED)
        st->codecpar->color_primaries = meta->primaries;
    
    if (meta->trc != AVCOL_TRC_RESERVED)
        st->codecpar->color_trc = meta->trc;

    // 添加Content Light Level元数据
    if (meta->max_cll && meta->max_fall) {
        AVContentLightMetadata *metadata = 
            av_content_light_metadata_alloc(&size);
        
        av_packet_side_data_add(&st->codecpar->coded_side_data,
                               &st->codecpar->nb_coded_side_data,
                               AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
                               metadata, size, 0);
        
        metadata->MaxCLL = meta->max_cll;
        metadata->MaxFALL = meta->max_fall;
    }

    // 添加Mastering Display元数据
    if (has_mastering_primaries || has_mastering_luminance) {
        AVMasteringDisplayMetadata *metadata = 
            av_mastering_display_metadata_alloc_size(&size);
        
        av_packet_side_data_add(&st->codecpar->coded_side_data,
                               &st->codecpar->nb_coded_side_data,
                               AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
                               metadata, size, 0);
        
        // 设置亮度信息
        if (has_mastering_luminance) {
            metadata->max_luminance = av_d2q(mastering->max_luminance, INT_MAX);
            metadata->min_luminance = av_d2q(mastering->min_luminance, INT_MAX);
            metadata->has_luminance = 1;
        }
        
        // 设置色域信息
        if (has_mastering_primaries) {
            metadata->display_primaries[0][0] = av_d2q(mastering->r_x, INT_MAX);
            metadata->display_primaries[0][1] = av_d2q(mastering->r_y, INT_MAX);
            // ... 其他色域点
            metadata->has_primaries = 1;
        }
    }

    return 0;
}

多轨道解封装

10.1 多轨道概念

Enhanced RTMP支持在一个FLV文件中包含多个视频或音频轨道,每个轨道通过track_idx标识。

10.2 多轨道检测

c 复制代码
// 音频多轨道
if (pkt_type == AudioPacketTypeMultitrack) {
    uint8_t types = avio_r8(s->pb);
    multitrack_type = types & 0xF0;  // 高4位:轨道类型
    pkt_type = types & 0xF;          // 低4位:包类型
    multitrack = 1;
    size--;
}

// 视频多轨道
if (pkt_type == PacketTypeMultitrack) {
    uint8_t types = avio_r8(s->pb);
    multitrack_type = types & 0xF0;
    pkt_type = types & 0xF;
    multitrack = 1;
    size--;
}

// 读取轨道索引
if (multitrack) {
    track_idx = avio_r8(s->pb);
    size--;
}

10.3 多轨道类型

c 复制代码
enum {
    MultitrackTypeOneTrack             = 0x00,  // 单轨道
    MultitrackTypeManyTracks           = 0x10,  // 多轨道(相同编解码器)
    MultitrackTypeManyTracksManyCodecs = 0x20,  // 多轨道(不同编解码器)
};

10.4 多轨道数据读取

c 复制代码
for (;;) {
    int track_size = size;

    // 如果不是单轨道模式,读取轨道大小
    if (multitrack_type != MultitrackTypeOneTrack) {
        track_size = avio_rb24(s->pb);
        size -= 3;
    }

    // 查找或创建对应track_idx的流
    for (i = 0; i < s->nb_streams; i++) {
        st = s->streams[i];
        if (stream_type == FLV_STREAM_TYPE_AUDIO) {
            if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
                st->id == track_idx)  // 匹配track_idx
                break;
        } else if (stream_type == FLV_STREAM_TYPE_VIDEO) {
            if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
                st->id == track_idx)
                break;
        }
    }
    
    // 如果流不存在,创建新流
    if (i == s->nb_streams) {
        st = create_stream(s, stream_types[stream_type], track_idx);
    }

    // 读取轨道数据
    ret = av_get_packet(s->pb, pkt, track_size);
    
    // ... 设置AVPacket属性 ...
    
    // 如果还有更多轨道
    if (!size)
        break;

    if (multitrack_type == MultitrackTypeOneTrack) {
        av_log(s, AV_LOG_ERROR, 
               "Attempted to read next track in single-track mode.\n");
        break;
    }

    // 如果是多编解码器模式,读取下一个编解码器ID
    if (multitrack_type == MultitrackTypeManyTracksManyCodecs) {
        codec_id = avio_rb32(s->pb);
        size -= 4;
    }

    // 读取下一个轨道索引
    track_idx = avio_r8(s->pb);
    size--;
}

10.5 多轨道extradata管理

多轨道模式下,每个轨道可能有不同的extradata:

c 复制代码
static int flv_queue_extradata(FLVContext *flv, AVIOContext *pb, 
                               int stream, int size, int multitrack)
{
    if (!multitrack) {
        // 单轨道模式:使用new_extradata数组
        av_free(flv->new_extradata[stream]);
        flv->new_extradata[stream] = 
            av_mallocz(size + AV_INPUT_BUFFER_PADDING_SIZE);
        flv->new_extradata_size[stream] = size;
        avio_read(pb, flv->new_extradata[stream], size);
    } else {
        // 多轨道模式:使用mt_extradata数组
        int new_count = stream + 1;

        // 扩展数组
        if (flv->mt_extradata_cnt < new_count) {
            void *tmp = av_realloc_array(flv->mt_extradata, 
                                        new_count,
                                        sizeof(*flv->mt_extradata));
            flv->mt_extradata = tmp;
            
            tmp = av_realloc_array(flv->mt_extradata_sz, 
                                  new_count,
                                  sizeof(*flv->mt_extradata_sz));
            flv->mt_extradata_sz = tmp;
            
            flv->mt_extradata_cnt = new_count;
        }

        // 分配并读取extradata
        av_free(flv->mt_extradata[stream]);
        flv->mt_extradata[stream] = 
            av_mallocz(size + AV_INPUT_BUFFER_PADDING_SIZE);
        flv->mt_extradata_sz[stream] = size;
        avio_read(pb, flv->mt_extradata[stream], size);
    }

    return 0;
}

10.6 多声道音频配置

Enhanced RTMP支持超过立体声的多声道音频:

c 复制代码
if (pkt_type == AudioPacketTypeMultichannelConfig) {
    int channel_order = avio_r8(s->pb);
    channels = avio_r8(s->pb);
    size -= 2;

    av_channel_layout_uninit(&st->codecpar->ch_layout);

    if (channel_order == AudioChannelOrderCustom) {
        // 自定义声道布局
        ret = av_channel_layout_custom_init(&st->codecpar->ch_layout, channels);
        
        for (i = 0; i < channels; i++) {
            uint8_t id = avio_r8(s->pb);
            size--;
            
            // 映射FLV声道ID到FFmpeg声道ID
            if (id < 18)
                st->codecpar->ch_layout.u.map[i].id = id;
            else if (id >= 18 && id <= 23)
                st->codecpar->ch_layout.u.map[i].id = 
                    id - 18 + AV_CHAN_LOW_FREQUENCY_2;
            else if (id == 0xFE)
                st->codecpar->ch_layout.u.map[i].id = AV_CHAN_UNUSED;
            else
                st->codecpar->ch_layout.u.map[i].id = AV_CHAN_UNKNOWN;
        }
    } else if (channel_order == AudioChannelOrderNative) {
        // 原生声道布局(使用掩码)
        uint64_t mask = avio_rb32(s->pb);
        size -= 4;
        
        // FLV的前18个声道与FFmpeg一致,后6个从AV_CHAN_LOW_FREQUENCY_2开始
        mask = (mask & 0x3FFFF) | 
               ((mask & 0xFC0000) << (AV_CHAN_LOW_FREQUENCY_2 - 18));
        
        ret = av_channel_layout_from_mask(&st->codecpar->ch_layout, mask);
    } else {
        // 未指定声道布局,使用默认
        av_channel_layout_default(&st->codecpar->ch_layout, channels);
    }

    goto next_track;
}

错误处理与容错机制

11.1 重同步机制

当检测到Tag大小不匹配时,解封装器会尝试重新同步:

复制代码
resync()
    │
    ├─→ 记录当前位置
    │   └─ pos = avio_tell(pb)
    │
    ├─→ 循环读取字节
    │   └─ for (i = 0; !avio_feof(pb); i++)
    │       │
    │       ├─→ 读入循环缓冲区
    │       │   └─ resync_buffer[j] = avio_r8(pb)
    │       │
    │       ├─→ 检测FLV文件头(拼接文件)
    │       │   └─ if (d[0]=='F' && d[1]=='L' && d[2]=='V')
    │       │       ├─ 设置时间偏移
    │       │       └─ time_offset = last_ts + 1
    │       │
    │       └─→ 验证Tag结构
    │           ├─ 读取PreviousTagSize
    │           ├─ 验证Tag大小一致性
    │           └─ if (size1 == lsize1-11 && size2 == lsize2-11)
    │               └─ 找到同步点,返回
    │
    └─→ 返回结果
        ├─ 成功:返回1
        └─ 失败:返回AVERROR_EOF

11.2 Tag大小验证

c 复制代码
// 读取PreviousTagSize
last = avio_rb32(s->pb);

if (!flv->trust_datasize) {
    // 验证Tag大小
    if (last != orig_size + 11 &&    // 标准大小
        last != orig_size + 10 &&    // 某些编码器的错误
        !avio_feof(s->pb) &&
        (last != orig_size || !last) && 
        last != flv->sum_flv_tag_size &&
        !flv->broken_sizes) {
        
        av_log(s, AV_LOG_ERROR, 
               "Packet mismatch %d %d %"PRId64"\n", 
               last, orig_size + 11, flv->sum_flv_tag_size);
        
        // 尝试重新同步
        avio_seek(s->pb, pos + 1, SEEK_SET);
        ret = resync(s);
        av_packet_unref(pkt);
        
        if (ret >= 0) {
            goto retry;
        }
    }
}

11.3 broken_sizes检测

某些编码器(如旧版OBS)会产生错误的Tag大小:

c 复制代码
if (amf_type == AMF_DATA_TYPE_STRING) {
    if (!strcmp(key, "encoder")) {
        int version = -1;
        if (1 == sscanf(str_val, "Open Broadcaster Software v0.%d", &version)) {
            if (version > 0 && version <= 655)
                flv->broken_sizes = 1;
        }
    } else if (!strcmp(key, "metadatacreator")) {
        if (!strcmp(str_val, "MEGA") ||
            !strncmp(str_val, "FlixEngine", 10))
            flv->broken_sizes = 1;
    }
}

11.4 时间戳异常处理

c 复制代码
// 检测负CTS
if (cts < 0) {
    if (!flv->wrong_dts)
        av_log(s, AV_LOG_WARNING,
            "Negative cts, previous timestamps might be wrong.\n");
    flv->wrong_dts = 1;
}

// 检测时间戳跳变
else if (FFABS(dts - pts) > 1000*60*15) {  // 超过15分钟
    av_log(s, AV_LOG_WARNING,
           "invalid timestamps %"PRId64" %"PRId64"\n", dts, pts);
    dts = pts = AV_NOPTS_VALUE;
}

11.5 拼接文件处理

FLV文件可能被拼接在一起,需要处理时间戳跳变:

c 复制代码
// 检测到新的FLV文件头
if (d[0] == 'F' && d[1] == 'L' && d[2] == 'V' && d[3] < 5 && d[5] == 0) {
    av_log(s, AV_LOG_WARNING, 
           "Concatenated FLV detected, might fail to demux, decode and seek %"PRId64"\n", 
           flv->last_ts);
    
    // 设置时间偏移
    flv->time_offset = flv->last_ts + 1;
    flv->time_pos = avio_tell(s->pb);
}

// 应用时间偏移
if (flv->time_pos <= pos) {
    dts += flv->time_offset;
}

性能优化与最佳实践

12.1 内存管理优化

  1. 循环缓冲区:重同步使用循环缓冲区,避免大量内存分配
c 复制代码
uint8_t resync_buffer[2*RESYNC_BUFFER_SIZE];
int j = i & (RESYNC_BUFFER_SIZE-1);
int j1 = j + RESYNC_BUFFER_SIZE;
flv->resync_buffer[j] = flv->resync_buffer[j1] = avio_r8(s->pb);
  1. 延迟分配:流和extradata按需分配,不预先分配所有可能的流

  2. 及时释放:关键帧索引在使用后立即释放

12.2 I/O优化

  1. 批量读取 :使用av_get_packet批量读取Tag数据
  2. 跳过不需要的数据 :使用avio_skip而不是逐字节读取
  3. 索引加速Seek:利用关键帧索引实现快速定位

12.3 解析优化

  1. 递归深度限制:AMF解析限制递归深度为16,防止栈溢出
c 复制代码
#define MAX_DEPTH 16

if (depth > MAX_DEPTH)
    return AVERROR_PATCHWELCOME;
  1. 提前返回:检测到错误立即返回,不继续解析

  2. 缓存状态:缓存last_sample_rate、last_channels等,减少重复计算

12.4 最佳实践建议

  1. 启用trust_metadata:如果确信元数据正确,可以加快流创建
  2. 使用关键帧索引:对于大文件,关键帧索引能显著提升Seek性能
  3. 处理直播流 :使用live_flv格式处理RTMP直播流
  4. 验证文件完整性:检查PreviousTagSize以发现文件损坏
  5. 处理时间戳异常:对于可能有时间戳问题的文件,启用容错机制

总结

本文深入解析了FFmpeg中FLV解封装器的实现原理,从格式探测到数据提取,从元数据解析到错误处理,涵盖了解封装器的方方面面。

13.1 核心要点回顾

  1. 解封装流程:探测 → 读取头部 → 循环读取Tag → 构建AVPacket
  2. Tag类型处理:音频Tag、视频Tag、脚本Tag各有不同的解析逻辑
  3. 元数据解析:使用AMF格式,递归解析对象和数组
  4. 关键帧索引:从onMetaData中提取,用于快速Seek
  5. Enhanced RTMP:支持新编解码器、HDR、多轨道

13.2 技术亮点

  1. 容错机制:重同步、broken_sizes检测、时间戳异常处理
  2. 多轨道支持:通过track_idx区分不同轨道
  3. HDR支持:解析colorInfo元数据,设置色彩空间参数
  4. 索引验证:通过验证点机制确保索引准确性
  5. 拼接文件处理:自动检测并处理时间戳偏移

13.3 应用场景

FLV解封装器广泛应用于:

  • 视频播放器(播放FLV文件)
  • 直播客户端(接收RTMP流)
  • 视频转码工具(读取FLV文件)
  • 视频分析工具(提取元数据和关键帧)

13.4 未来展望

随着流媒体技术的发展,FLV解封装器也在不断演进:

  • 更完善的Enhanced RTMP支持
  • 更好的错误恢复能力
  • 更高效的多轨道处理
  • 更准确的时间戳处理

理解FFmpeg FLV解封装器的实现原理,不仅有助于更好地使用FFmpeg工具,也为开发自定义的流媒体应用提供了宝贵的参考。


参考文献

  1. Adobe Systems Incorporated. (2009). "Adobe Flash Video File Format Specification Version 10.1"
  2. Enhanced RTMP Specification. https://github.com/veovera/enhanced-rtmp
  3. FFmpeg Documentation. https://ffmpeg.org/documentation.html
  4. FFmpeg Source Code. https://github.com/FFmpeg/FFmpeg
  5. AMF (Action Message Format) Specification. Adobe Systems

相关推荐
报错小能手2 小时前
C++ STL bitset 位图
开发语言·c++
橘子真甜~2 小时前
Reids命令原理与应用1 - Redis命令与原理
数据库·c++·redis·缓存
钓鱼的肝2 小时前
GESP系列(3级)小杨的储蓄
开发语言·数据结构·c++·笔记·算法·gesp
一个不知名程序员www3 小时前
算法学习入门---C/C++输入输出
c语言·c++
行业探路者3 小时前
如何利用活码生成产品画册二维码?
学习·音视频·语音识别·二维码·设备巡检
web前端进阶者3 小时前
webRTC指定设备加自定义用户头像
音视频·webrtc
qq_433554543 小时前
C++ 状压DP(01矩阵约束问题)
c++·算法·矩阵
千里马-horse3 小时前
CallbackInfo
c++·node.js·napi·callbackinfo