FFmpeg FLV解码器原理深度解析:从格式解析到数据提取的完整流程
目录
- 引言
- FLV解封装概述
- [FFmpeg FLV解码器架构](#FFmpeg FLV解码器架构)
- 核心数据结构解析
- 解封装流程详解
- 关键函数深度剖析
- 元数据解析机制
- 关键帧索引解析
- [Enhanced RTMP支持](#Enhanced RTMP支持)
- 多轨道解封装
- 错误处理与容错机制
- 性能优化与最佳实践
- 总结
引言
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;
}
探测要点:
- 文件签名:前3字节必须是"FLV"
- 版本号:第4字节,当前版本为1,小于5都可接受
- 保留字节:第6字节必须为0
- 头部偏移:通常为9,但需要验证合理性
- 直播流检测:通过特定标识区分直播流和点播文件
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 索引的作用
关键帧索引允许:
- 快速Seek:直接跳转到指定时间点
- 进度显示:计算播放进度
- 缩略图生成:提取关键帧作为缩略图
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 内存管理优化
- 循环缓冲区:重同步使用循环缓冲区,避免大量内存分配
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);
-
延迟分配:流和extradata按需分配,不预先分配所有可能的流
-
及时释放:关键帧索引在使用后立即释放
12.2 I/O优化
- 批量读取 :使用
av_get_packet批量读取Tag数据 - 跳过不需要的数据 :使用
avio_skip而不是逐字节读取 - 索引加速Seek:利用关键帧索引实现快速定位
12.3 解析优化
- 递归深度限制:AMF解析限制递归深度为16,防止栈溢出
c
#define MAX_DEPTH 16
if (depth > MAX_DEPTH)
return AVERROR_PATCHWELCOME;
-
提前返回:检测到错误立即返回,不继续解析
-
缓存状态:缓存last_sample_rate、last_channels等,减少重复计算
12.4 最佳实践建议
- 启用trust_metadata:如果确信元数据正确,可以加快流创建
- 使用关键帧索引:对于大文件,关键帧索引能显著提升Seek性能
- 处理直播流 :使用
live_flv格式处理RTMP直播流 - 验证文件完整性:检查PreviousTagSize以发现文件损坏
- 处理时间戳异常:对于可能有时间戳问题的文件,启用容错机制
总结
本文深入解析了FFmpeg中FLV解封装器的实现原理,从格式探测到数据提取,从元数据解析到错误处理,涵盖了解封装器的方方面面。
13.1 核心要点回顾
- 解封装流程:探测 → 读取头部 → 循环读取Tag → 构建AVPacket
- Tag类型处理:音频Tag、视频Tag、脚本Tag各有不同的解析逻辑
- 元数据解析:使用AMF格式,递归解析对象和数组
- 关键帧索引:从onMetaData中提取,用于快速Seek
- Enhanced RTMP:支持新编解码器、HDR、多轨道
13.2 技术亮点
- 容错机制:重同步、broken_sizes检测、时间戳异常处理
- 多轨道支持:通过track_idx区分不同轨道
- HDR支持:解析colorInfo元数据,设置色彩空间参数
- 索引验证:通过验证点机制确保索引准确性
- 拼接文件处理:自动检测并处理时间戳偏移
13.3 应用场景
FLV解封装器广泛应用于:
- 视频播放器(播放FLV文件)
- 直播客户端(接收RTMP流)
- 视频转码工具(读取FLV文件)
- 视频分析工具(提取元数据和关键帧)
13.4 未来展望
随着流媒体技术的发展,FLV解封装器也在不断演进:
- 更完善的Enhanced RTMP支持
- 更好的错误恢复能力
- 更高效的多轨道处理
- 更准确的时间戳处理
理解FFmpeg FLV解封装器的实现原理,不仅有助于更好地使用FFmpeg工具,也为开发自定义的流媒体应用提供了宝贵的参考。
参考文献
- Adobe Systems Incorporated. (2009). "Adobe Flash Video File Format Specification Version 10.1"
- Enhanced RTMP Specification. https://github.com/veovera/enhanced-rtmp
- FFmpeg Documentation. https://ffmpeg.org/documentation.html
- FFmpeg Source Code. https://github.com/FFmpeg/FFmpeg
- AMF (Action Message Format) Specification. Adobe Systems