FFmpeg FLV编码器原理深度解析:从代码到实现的完整流程
目录
- 引言
- FLV格式概述
- [FFmpeg FLV编码器架构](#FFmpeg FLV编码器架构)
- 核心数据结构解析
- 编码流程详解
- 关键函数深度剖析
- 元数据处理机制
- 关键帧索引系统
- 多轨道支持
- 性能优化与最佳实践
- 总结
引言
FLV(Flash Video)格式作为Adobe公司开发的流媒体容器格式,在视频直播、点播等领域有着广泛的应用。虽然Flash技术已经逐渐退出历史舞台,但FLV格式由于其良好的兼容性和流式传输特性,仍然被许多流媒体平台采用。FFmpeg作为最强大的多媒体处理框架之一,其FLV编码器(flvenc.c)实现了完整的FLV文件封装功能。
本文将从源码层面深入解析FFmpeg中FLV编码器的实现原理,通过详细的代码分析和流程图,帮助读者全面理解FLV编码的完整流程。文章将涵盖从初始化到数据包写入、从元数据生成到关键帧索引构建的每一个环节,力求做到深入浅出、通俗易懂。
FLV格式概述
2.1 FLV文件结构
FLV文件采用Tag-based的流式结构,整个文件由三部分组成:
┌─────────────────────────────────────────┐
│ FLV File Header (9 bytes) │
│ - Signature: "FLV" (3 bytes) │
│ - Version: 1 (1 byte) │
│ - Flags: HasAudio/HasVideo (1 byte) │
│ - HeaderSize: 9 (4 bytes) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ PreviousTagSize0 (4 bytes) │
│ - Always 0 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Tag 1 │
│ ┌───────────────────────────────────┐ │
│ │ Tag Header (11 bytes) │ │
│ │ - TagType (1 byte) │ │
│ │ - DataSize (3 bytes) │ │
│ │ - Timestamp (4 bytes) │ │
│ │ - StreamID (3 bytes) │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ Tag Data (variable size) │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ PreviousTagSize1 (4 bytes) │
│ - Size of Tag 1 + 11 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Tag 2 │
│ ... │
└─────────────────────────────────────────┘
2.2 Tag类型
FLV定义了三种主要的Tag类型:
- 音频Tag (0x08): 包含音频编码数据
- 视频Tag (0x09): 包含视频编码数据
- 脚本Tag (0x12): 包含元数据信息(onMetaData)
2.3 时间戳机制
FLV使用32位时间戳,单位为毫秒。时间戳分为两部分:
- 低24位:存储在Tag Header的Timestamp字段
- 高8位:存储在Tag Header的TimestampExtended字段
这种设计允许FLV文件支持最长约24小时(2^31毫秒)的视频内容。
FFmpeg FLV编码器架构
3.1 模块组织结构
FFmpeg的FLV编码器位于libavformat/flvenc.c文件中,它实现了FFmpeg的AVOutputFormat接口。整个编码器采用面向对象的设计思想,通过函数指针实现多态。
AVFormatContext
│
├── AVOutputFormat (ff_flv_muxer)
│ │
│ ├── init: flv_init
│ ├── write_header: flv_write_header
│ ├── write_packet: flv_write_packet
│ ├── write_trailer: flv_write_trailer
│ └── deinit: flv_deinit
│
└── FLVContext (私有数据)
│
├── 流信息
│ ├── audio_par
│ ├── video_par
│ └── data_par
│
├── 元数据偏移
│ ├── duration_offset
│ ├── filesize_offset
│ └── metadata_size_pos
│
└── 关键帧索引
├── filepositions
└── keyframe_index_size
3.2 编码器生命周期
FLV编码器的完整生命周期包括以下几个阶段:
开始
│
├─→ flv_init() [初始化阶段]
│ ├─ 分配内存
│ ├─ 验证流参数
│ └─ 设置时间戳信息
│
├─→ flv_write_header() [头部写入阶段]
│ ├─ 写入FLV文件头
│ ├─ 写入元数据Tag
│ └─ 写入编解码器头
│
├─→ flv_write_packet() [数据包写入阶段]
│ ├─ 循环处理每个AVPacket
│ ├─ 写入Tag Header
│ ├─ 写入Tag Data
│ └─ 更新统计信息
│
├─→ flv_write_trailer() [尾部写入阶段]
│ ├─ 更新元数据
│ ├─ 写入关键帧索引
│ └─ 写入EOS Tag
│
└─→ flv_deinit() [清理阶段]
└─ 释放资源
核心数据结构解析
4.1 FLVContext结构体
FLVContext是FLV编码器的核心数据结构,存储了编码过程中的所有状态信息:
c
typedef struct FLVContext {
AVClass *av_class; // AVClass用于选项系统
// 文件位置信息
int64_t duration_offset; // duration字段在文件中的偏移
int64_t filesize_offset; // filesize字段在文件中的偏移
int64_t datastart_offset; // 数据区开始位置
int64_t datasize_offset; // datasize字段偏移
int64_t videosize_offset; // videosize字段偏移
int64_t audiosize_offset; // audiosize字段偏移
// 元数据相关
int64_t metadata_size_pos; // 元数据大小位置
int64_t metadata_totalsize_pos; // 元数据总大小位置
int64_t metadata_totalsize; // 元数据总大小
// 时间戳信息
int64_t delay; // 第一个DTS的延迟值
int64_t lasttimestamp_offset; // 最后时间戳偏移
double lasttimestamp; // 最后时间戳值
int64_t lastkeyframetimestamp_offset;
double lastkeyframetimestamp;
int64_t lastkeyframelocation_offset;
int64_t lastkeyframelocation;
// 关键帧索引
int64_t keyframes_info_offset; // 关键帧信息偏移
int64_t keyframe_index_size; // 关键帧索引大小
int64_t filepositions_count; // 关键帧位置数量
FLVFileposition *filepositions; // 关键帧位置链表
FLVFileposition *head_filepositions;
// 流参数
AVCodecParameters *audio_par; // 音频流参数
AVCodecParameters *video_par; // 视频流参数
AVCodecParameters *data_par; // 数据流参数
double framerate; // 帧率
// 控制标志
int flags; // FLV标志位
int64_t *last_ts; // 每个流的最后时间戳
int *metadata_pkt_written; // 元数据包是否已写入
int *track_idx_map; // 轨道索引映射
} FLVContext;
4.2 FLVFileposition结构体
用于存储关键帧的位置信息,构成链表结构:
c
typedef struct FLVFileposition {
int64_t keyframe_position; // 关键帧在文件中的位置
double keyframe_timestamp; // 关键帧的时间戳
struct FLVFileposition *next; // 指向下一个节点
} FLVFileposition;
4.3 FLVFlags枚举
定义了FLV编码器的各种标志位:
c
typedef enum {
FLV_AAC_SEQ_HEADER_DETECT = (1 << 0), // 检测AAC序列头
FLV_NO_SEQUENCE_END = (1 << 1), // 不写入序列结束Tag
FLV_ADD_KEYFRAME_INDEX = (1 << 2), // 添加关键帧索引
FLV_NO_METADATA = (1 << 3), // 不写入元数据
FLV_NO_DURATION_FILESIZE = (1 << 4), // 不写入duration和filesize
} FLVFlags;
编码流程详解
5.1 初始化阶段 (flv_init)
flv_init函数负责编码器的初始化工作,这是整个编码流程的第一步。
5.1.1 函数执行流程
flv_init()
│
├─→ 分配内存
│ ├─ last_ts数组 (每个流的时间戳)
│ ├─ metadata_pkt_written数组
│ └─ track_idx_map数组
│
├─→ 遍历所有流
│ │
│ ├─→ 视频流处理
│ │ ├─ 验证多轨道视频编解码器
│ │ ├─ 计算帧率
│ │ ├─ 设置track_idx_map
│ │ ├─ 验证编解码器兼容性
│ │ └─ 检查MPEG4/H263的严格性
│ │
│ ├─→ 音频流处理
│ │ ├─ 验证多轨道音频编解码器
│ │ ├─ 设置track_idx_map
│ │ ├─ 获取音频标志
│ │ └─ 检查PCM格式警告
│ │
│ └─→ 数据流处理
│ └─ 验证编解码器类型
│
├─→ 设置PTS信息
│ └─ avpriv_set_pts_info(32, 1, 1000)
│ (32位PTS,时间基1/1000,即毫秒)
│
└─→ 初始化delay为AV_NOPTS_VALUE
5.1.2 关键代码分析
内存分配:
c
flv->last_ts = av_calloc(s->nb_streams, sizeof(*flv->last_ts));
flv->metadata_pkt_written = av_calloc(s->nb_streams, sizeof(*flv->metadata_pkt_written));
flv->track_idx_map = av_calloc(s->nb_streams, sizeof(*flv->track_idx_map));
这里为每个流分配了独立的时间戳数组和元数据写入标志数组,支持多轨道编码。
视频流验证:
c
if (video_ctr &&
par->codec_id != AV_CODEC_ID_VP8 &&
par->codec_id != AV_CODEC_ID_VP9 &&
par->codec_id != AV_CODEC_ID_AV1 &&
par->codec_id != AV_CODEC_ID_H264 &&
par->codec_id != AV_CODEC_ID_HEVC) {
av_log(s, AV_LOG_ERROR, "Unsupported multi-track video codec.\n");
return AVERROR(EINVAL);
}
多轨道视频只支持特定的编解码器,这是FLV格式的限制。
编解码器兼容性检查:
c
if (!ff_codec_get_tag(flv_video_codec_ids, par->codec_id))
return unsupported_codec(s, "Video", par->codec_id);
通过查找编解码器ID映射表来验证编解码器是否被FLV支持。
5.2 头部写入阶段 (flv_write_header)
flv_write_header函数负责写入FLV文件的头部信息,包括文件头、元数据Tag和编解码器头。
5.2.1 FLV文件头结构
FLV文件头共9字节:
字节位置 大小 内容 说明
0-2 3 "FLV" 文件签名
3 1 0x01 版本号
4 1 Flags 标志位
bit0: 保留
bit2: HasAudio (1=有音频)
bit4: HasVideo (1=有视频)
5-8 4 0x00000009 头部大小(固定为9)
5.2.2 函数执行流程
flv_write_header()
│
├─→ 写入FLV文件头
│ ├─ "FLV" (3 bytes)
│ ├─ Version: 1 (1 byte)
│ ├─ Flags: HasAudio|HasVideo (1 byte)
│ └─ HeaderSize: 9 (4 bytes)
│
├─→ 处理特殊流(codec_tag == 5)
│ └─ 写入8字节消息类型Tag
│
├─→ 写入元数据Tag(如果未禁用)
│ └─ write_metadata(s, 0)
│
├─→ 写入编解码器头
│ └─ 遍历所有流
│ └─ flv_write_codec_header()
│
└─→ 记录数据区开始位置
└─ flv->datastart_offset = avio_tell(pb)
5.2.3 元数据写入详解
write_metadata函数生成onMetaData脚本Tag,这是FLV文件最重要的元数据信息。
元数据结构:
FLV Tag Header (11 bytes)
TagType: 0x12 (脚本Tag)
DataSize: (待填充)
Timestamp: 0
StreamID: 0
Tag Data:
AMF_DATA_TYPE_STRING: "onMetaData"
AMF_DATA_TYPE_MIXEDARRAY:
ArraySize: (元数据项数量)
元数据项列表:
"duration" -> double (时长,秒)
"width" -> double (视频宽度)
"height" -> double (视频高度)
"videodatarate" -> double (视频码率,kbps)
"framerate" -> double (帧率)
"videocodecid" -> double (视频编解码器ID)
"audiodatarate" -> double (音频码率,kbps)
"audiosamplerate" -> double (采样率)
"audiosamplesize" -> double (采样大小)
"stereo" -> bool (是否立体声)
"audiocodecid" -> double (音频编解码器ID)
"filesize" -> double (文件大小,字节)
... (用户自定义元数据)
"" -> AMF_END_OF_OBJECT
延迟写入机制:
由于duration和filesize在编码完成前无法确定,FLV编码器采用了延迟写入机制:
- 在写入元数据时,先写入占位符值(0或估算值)
- 记录这些字段在文件中的偏移位置
- 在
flv_write_trailer中,回写到这些位置更新真实值
c
// 写入duration占位符
put_amf_string(pb, "duration");
flv->duration_offset = avio_tell(pb); // 记录偏移
put_amf_double(pb, s->duration / AV_TIME_BASE); // 写入占位值
// 写入filesize占位符
put_amf_string(pb, "filesize");
flv->filesize_offset = avio_tell(pb); // 记录偏移
put_amf_double(pb, 0); // 写入0作为占位符
5.2.4 编解码器头写入
flv_write_codec_header函数为每个流写入编解码器特定的头信息。
支持的编解码器头类型:
-
AAC音频:
- 标准模式:AudioTagHeader + AAC Sequence Header
- 扩展模式:Extended AudioTagHeader + FourCC + AAC Sequence Header
-
H.264视频:
- 标准模式:VideoTagHeader + AVC Sequence Header
- 扩展模式:Extended VideoTagHeader + FourCC + AVC Sequence Header
-
HEVC/AV1/VP9视频:
- 仅支持扩展模式:Extended VideoTagHeader + FourCC + 编解码器配置
扩展头格式:
对于支持Enhanced RTMP的编解码器,使用扩展头格式:
Extended VideoTagHeader:
Byte 0: 0x80 | PacketType | FrameType
Byte 1-4: FourCC ("avc1", "hvc1", "av01", "vp09")
Byte 5+: 编解码器特定数据
5.3 数据包写入阶段 (flv_write_packet)
flv_write_packet是编码器的核心函数,负责将AVPacket转换为FLV Tag并写入文件。
5.3.1 函数执行流程图
flv_write_packet(pkt)
│
├─→ 预处理阶段
│ ├─ 获取流参数 (par)
│ ├─ 计算flags_size(Tag Data头部大小)
│ ├─ 检查extradata更新
│ └─ 写入元数据包(如果需要)
│
├─→ 时间戳处理
│ ├─ 初始化delay(第一个包)
│ ├─ 验证DTS顺序
│ └─ 计算FLV时间戳
│
├─→ 写入Tag Header
│ ├─ TagType (Audio/Video/Meta)
│ ├─ DataSize (3 bytes)
│ ├─ Timestamp (4 bytes)
│ └─ StreamID (3 bytes, 通常为0)
│
├─→ 写入Tag Data Header
│ │
│ ├─→ 视频Tag
│ │ ├─ 扩展视频头(HEVC/AV1/VP9/H264多轨道)
│ │ │ ├─ Extended Header标志
│ │ │ ├─ PacketType
│ │ │ ├─ FrameType
│ │ │ ├─ FourCC
│ │ │ └─ TrackIdx(多轨道)
│ │ │ └─ CompositionTime(H264/HEVC,如果PTS!=DTS)
│ │ │
│ │ └─ 标准视频头(其他编解码器)
│ │ ├─ CodecID | FrameType
│ │ └─ PacketType(H264/MPEG4)
│ │
│ ├─→ 音频Tag
│ │ ├─ 扩展音频头(OPUS/FLAC/AC3/EAC3/多轨道AAC)
│ │ │ ├─ Extended Header标志
│ │ │ ├─ PacketType
│ │ │ ├─ FourCC
│ │ │ └─ TrackIdx(多轨道)
│ │ │
│ │ └─ 标准音频头(其他编解码器)
│ │ └─ AudioFlags (CodecID|SampleRate|SampleSize|Channels)
│ │
│ └─→ 脚本Tag(数据流)
│ └─ AMF格式数据
│
├─→ 数据转换
│ ├─ H264/MPEG4: Annex-B -> MP4格式转换
│ ├─ HEVC: Annex-B -> MP4格式转换
│ └─ 其他: 直接写入
│
├─→ 写入Tag Data
│ └─ avio_write(pb, data, size)
│
├─→ 写入PreviousTagSize
│ └─ avio_wb32(pb, size + flags_size + 11)
│
└─→ 更新统计信息
├─ 更新duration
├─ 更新关键帧索引(如果启用)
└─ 更新视频/音频大小统计
5.3.2 时间戳处理详解
FLV编码器需要处理复杂的时间戳转换:
DTS延迟计算:
c
if (flv->delay == AV_NOPTS_VALUE)
flv->delay = -pkt->dts;
第一个包的DTS值被记录为延迟值,后续所有时间戳都会加上这个延迟,确保FLV文件的时间戳从0开始。
FLV时间戳计算:
c
ts = pkt->dts; // 使用DTS作为FLV时间戳
FLV使用DTS作为Tag的时间戳,因为FLV是流式格式,需要按照解码顺序排列。
时间戳验证:
c
if (pkt->dts < -flv->delay) {
av_log(s, AV_LOG_WARNING,
"Packets are not in the proper order with respect to DTS\n");
return AVERROR(EINVAL);
}
确保所有包的DTS都大于等于第一个包的DTS(即大于等于-flv->delay)。
5.3.3 视频Tag写入详解
标准视频Tag格式(H.264单轨道):
Tag Header (11 bytes)
TagType: 0x09
DataSize: N + 5
Timestamp: DTS
StreamID: 0
Tag Data:
Byte 0: CodecID(4 bits) | FrameType(4 bits)
CodecID = 7 (H.264)
FrameType = 1 (KeyFrame) 或 2 (InterFrame)
Byte 1: PacketType
0 = AVC Sequence Header
1 = AVC NALU
2 = AVC End of Sequence
Byte 2-4: CompositionTime (PTS - DTS,仅NALU包)
Byte 5+: 视频数据
扩展视频Tag格式(HEVC/AV1/VP9):
Tag Header (11 bytes)
TagType: 0x09
DataSize: N + 5+
Timestamp: DTS
StreamID: 0
Tag Data:
Byte 0: 0x80 | PacketType | FrameType
0x80 = Extended Header标志
PacketType = 0 (SequenceStart) 或 1 (CodedFrames)
FrameType = 1 (KeyFrame) 或 2 (InterFrame)
Byte 1-4: FourCC
"hvc1" (HEVC)
"av01" (AV1)
"vp09" (VP9)
Byte 5+: 视频数据(或CompositionTime + 视频数据)
多轨道视频Tag:
对于多轨道视频,在扩展头后添加TrackIdx:
Tag Data:
Byte 0: 0x80 | 0x06 | PacketType | FrameType
0x06 = Multitrack标志
Byte 1: 0x00 | PacketType
0x00 = MultitrackTypeOneTrack
Byte 2-5: FourCC
Byte 6: TrackIdx (轨道索引)
Byte 7+: 视频数据
5.3.4 音频Tag写入详解
标准音频Tag格式(AAC单轨道):
Tag Header (11 bytes)
TagType: 0x08
DataSize: N + 2
Timestamp: DTS
StreamID: 0
Tag Data:
Byte 0: AudioFlags
Bit 0: SoundType (0=Mono, 1=Stereo)
Bit 1: SoundSize (0=8bit, 1=16bit)
Bit 2-3: SoundRate (0=5.5kHz, 1=11kHz, 2=22kHz, 3=44kHz)
Bit 4-7: SoundFormat (10=AAC)
Byte 1: AACPacketType
0 = AAC Sequence Header
1 = AAC Raw
Byte 2+: 音频数据
扩展音频Tag格式(OPUS/FLAC/AC3/EAC3):
Tag Header (11 bytes)
TagType: 0x08
DataSize: N + 5+
Timestamp: DTS
StreamID: 0
Tag Data:
Byte 0: 0x90 | PacketType
0x90 = Extended Header标志 (CodecID=9)
PacketType = 0 (SequenceStart) 或 1 (CodedFrames)
Byte 1-4: FourCC
"Opus" (OPUS)
"fLaC" (FLAC)
"ac-3" (AC3)
"ec-3" (EAC3)
Byte 5+: 音频数据
5.3.5 数据格式转换
某些编解码器需要格式转换:
H.264 Annex-B -> MP4格式:
H.264数据在FFmpeg内部通常使用Annex-B格式(起始码:0x00000001),但FLV需要MP4格式(长度前缀)。
c
if (par->codec_id == AV_CODEC_ID_H264 || par->codec_id == AV_CODEC_ID_MPEG4) {
if (par->extradata_size > 0 && *(uint8_t*)par->extradata != 1)
if ((ret = ff_nal_parse_units_buf(pkt->data, &data, &size)) < 0)
return ret;
}
ff_nal_parse_units_buf函数将Annex-B格式转换为MP4格式:
- 查找起始码(0x00000001或0x000001)
- 提取NALU
- 将每个NALU的长度(4字节大端)写入,后跟NALU数据
HEVC Annex-B -> MP4格式:
类似H.264,但使用专门的转换函数:
c
if (par->codec_id == AV_CODEC_ID_HEVC) {
if (par->extradata_size > 0 && *(uint8_t*)par->extradata != 1)
if ((ret = ff_hevc_annexb2mp4_buf(pkt->data, &data, &size, 0, NULL)) < 0)
return ret;
}
5.4 尾部写入阶段 (flv_write_trailer)
flv_write_trailer函数在编码结束时被调用,负责更新元数据、写入关键帧索引和EOS Tag。
5.4.1 函数执行流程
flv_write_trailer()
│
├─→ 更新关键帧索引元数据(如果启用)
│ ├─ 更新videosize
│ ├─ 更新audiosize
│ ├─ 更新lasttimestamp
│ ├─ 更新lastkeyframetimestamp
│ ├─ 更新lastkeyframelocation
│ ├─ shift_data() (移动数据,为索引腾出空间)
│ └─ 写入关键帧索引
│ ├─ "filepositions"数组
│ └─ "times"数组
│
├─→ 写入EOS Tag(如果未禁用)
│ └─ 遍历视频流
│ └─ put_eos_tag() (H.264/MPEG4)
│
├─→ 更新duration和filesize
│ ├─ 回写duration
│ └─ 回写filesize
│
└─→ 完成
5.4.2 关键帧索引构建
关键帧索引是FLV文件的一个重要特性,它允许播放器快速定位和跳转到任意时间点。
索引结构:
在onMetaData中添加keyframes对象:
"keyframes": {
"filepositions": [pos1, pos2, pos3, ...],
"times": [ts1, ts2, ts3, ...]
}
filepositions: 每个关键帧在文件中的字节偏移times: 每个关键帧的时间戳(秒)
索引构建过程:
- 收集阶段(在flv_write_packet中):
c
if (pkt->flags & AV_PKT_FLAG_KEY) {
flv->lastkeyframetimestamp = flv->lasttimestamp;
flv->lastkeyframelocation = cur_offset;
ret = flv_append_keyframe_info(s, flv, flv->lasttimestamp, cur_offset);
}
每个关键帧都会被添加到链表中。
- 写入阶段(在flv_write_trailer中):
由于索引大小未知,需要先移动文件数据:
c
res = shift_data(s); // 移动数据,为索引腾出空间
shift_data函数计算索引大小,然后调用ff_format_shift_data移动文件数据。
- 写入索引数据:
c
put_amf_string(pb, "filepositions");
put_amf_dword_array(pb, flv->filepositions_count);
for (newflv_posinfo = flv->head_filepositions; newflv_posinfo; ...) {
put_amf_double(pb, newflv_posinfo->keyframe_position + flv->keyframe_index_size);
}
put_amf_string(pb, "times");
put_amf_dword_array(pb, flv->filepositions_count);
for (newflv_posinfo = flv->head_filepositions; newflv_posinfo; ...) {
put_amf_double(pb, newflv_posinfo->keyframe_timestamp);
}
注意:filepositions需要加上keyframe_index_size,因为索引是在数据之后插入的,导致所有数据位置后移。
5.4.3 EOS Tag写入
EOS(End of Sequence)Tag标记流的结束,主要用于H.264和MPEG4:
c
static void put_eos_tag(AVIOContext *pb, unsigned ts, enum AVCodecID codec_id)
{
uint32_t tag = ff_codec_get_tag(flv_video_codec_ids, codec_id);
tag |= 1 << 4; // FrameType = KeyFrame
avio_w8(pb, FLV_TAG_TYPE_VIDEO);
avio_wb24(pb, 5); // DataSize = 5
put_timestamp(pb, ts);
avio_wb24(pb, 0); // StreamID = 0
avio_w8(pb, tag);
avio_w8(pb, 2); // PacketType = AVC End of Sequence
avio_wb24(pb, 0); // Always 0
avio_wb32(pb, 16); // PreviousTagSize = 5 + 11
}
EOS Tag的结构:
- TagType: 0x09 (Video)
- DataSize: 5
- Tag Data: CodecID|FrameType + PacketType(2) + Reserved(3 bytes)
关键函数深度剖析
6.1 get_audio_flags函数
get_audio_flags函数根据音频流参数生成FLV音频Tag的Flags字节。
6.1.1 函数逻辑
get_audio_flags(par)
│
├─→ 特殊编解码器处理
│ ├─ AAC: 强制返回固定值
│ │ └─ FLV_CODECID_AAC | FLV_SAMPLERATE_44100HZ |
│ │ FLV_SAMPLESSIZE_16BIT | FLV_STEREO
│ │
│ ├─ OPUS/FLAC/AC3/EAC3: 返回EX_HEADER标志
│ │ └─ FLV_CODECID_EX_HEADER
│ │
│ └─ Speex: 验证参数并返回
│ ├─ 采样率必须16kHz
│ ├─ 声道必须单声道
│ └─ FLV_CODECID_SPEEX | FLV_SAMPLERATE_11025HZ |
│ FLV_SAMPLESSIZE_16BIT
│
├─→ 采样率处理
│ ├─ 48000Hz: MP3特殊处理(使用44.1kHz标识)
│ ├─ 44100Hz: FLV_SAMPLERATE_44100HZ
│ ├─ 22050Hz: FLV_SAMPLERATE_22050HZ
│ ├─ 11025Hz: FLV_SAMPLERATE_11025HZ
│ ├─ 16000/8000/5512Hz: FLV_SAMPLERATE_SPECIAL(非MP3)
│ └─ 其他: 错误
│
├─→ 声道处理
│ └─ 多声道: flags |= FLV_STEREO
│
└─→ 编解码器ID处理
├─ MP3: FLV_CODECID_MP3 | FLV_SAMPLESSIZE_16BIT
├─ PCM_U8: FLV_CODECID_PCM | FLV_SAMPLESSIZE_8BIT
├─ PCM_S16BE: FLV_CODECID_PCM | FLV_SAMPLESSIZE_16BIT
├─ PCM_S16LE: FLV_CODECID_PCM_LE | FLV_SAMPLESSIZE_16BIT
├─ ADPCM_SWF: FLV_CODECID_ADPCM | FLV_SAMPLESSIZE_16BIT
├─ NELLYMOSER: 根据采样率选择
├─ PCM_MULAW: FLV_CODECID_PCM_MULAW | FLV_SAMPLESSIZE_16BIT
├─ PCM_ALAW: FLV_CODECID_PCM_ALAW | FLV_SAMPLESSIZE_16BIT
└─ 其他: 错误
6.1.2 Flags字节结构
FLV音频Flags字节的位布局:
Bit 7 6 5 4 3 2 1 0
┌────┬────┬───┬──────────┐
│CodecID│SR│SS│ST│
└────┴────┴───┴──────────┘
CodecID (4 bits, Bit 4-7):
0 = PCM
1 = ADPCM
2 = MP3
3 = PCM LE
4 = Nellymoser 16kHz
5 = Nellymoser 8kHz
6 = Nellymoser
7 = PCM A-law
8 = PCM μ-law
9 = Reserved (用于扩展头)
10 = AAC
11 = Speex
SR - SampleRate (2 bits, Bit 2-3):
0 = 5.5kHz (特殊)
1 = 11kHz
2 = 22kHz
3 = 44.1kHz
SS - SampleSize (1 bit, Bit 1):
0 = 8-bit
1 = 16-bit
ST - SoundType (1 bit, Bit 0):
0 = Mono
1 = Stereo
6.2 write_metadata函数
write_metadata函数生成onMetaData脚本Tag,这是FLV文件最重要的元数据。
6.2.1 AMF格式详解
AMF(Action Message Format)是Adobe开发的数据序列化格式,用于Flash和RTMP协议。
AMF数据类型:
AMF_DATA_TYPE_NUMBER (0x00): 64位双精度浮点数AMF_DATA_TYPE_BOOL (0x01): 布尔值(1字节)AMF_DATA_TYPE_STRING (0x02): UTF-8字符串- 2字节长度(大端)
- 字符串数据
AMF_DATA_TYPE_OBJECT (0x03): 对象- 键值对列表
- 以空字符串+0x09结束
AMF_DATA_TYPE_NULL (0x05): null值AMF_DATA_TYPE_MIXEDARRAY (0x08): 混合数组- 4字节数组大小(大端)
- 键值对列表
- 以空字符串+0x09结束
6.2.2 元数据写入流程
write_metadata()
│
├─→ 写入Tag Header
│ ├─ TagType: 0x12
│ ├─ DataSize: 0 (占位,稍后填充)
│ ├─ Timestamp: ts
│ └─ StreamID: 0
│
├─→ 写入事件名
│ └─ AMF_STRING: "onMetaData"
│
├─→ 写入混合数组
│ ├─ AMF_MIXEDARRAY
│ ├─ ArraySize: metadata_count (占位,稍后填充)
│ │
│ ├─→ 基础元数据
│ │ ├─ "duration" -> double (如果启用)
│ │ ├─ "width" -> double (视频)
│ │ ├─ "height" -> double (视频)
│ │ ├─ "videodatarate" -> double (视频)
│ │ ├─ "framerate" -> double (视频,如果>0)
│ │ ├─ "videocodecid" -> double (视频)
│ │ ├─ "audiodatarate" -> double (音频)
│ │ ├─ "audiosamplerate" -> double (音频)
│ │ ├─ "audiosamplesize" -> double (音频)
│ │ ├─ "stereo" -> bool (音频)
│ │ ├─ "audiocodecid" -> double (音频)
│ │ ├─ "datastream" -> double (数据流)
│ │ └─ "filesize" -> double (如果启用)
│ │
│ ├─→ 用户元数据
│ │ └─ 遍历s->metadata字典
│ │ └─ 过滤保留关键字
│ │ └─ "key" -> AMF_STRING: "value"
│ │
│ └─→ 关键帧索引元数据(如果启用)
│ ├─ "hasVideo" -> bool
│ ├─ "hasKeyframes" -> bool
│ ├─ "hasAudio" -> bool
│ ├─ "hasMetadata" -> bool
│ ├─ "canSeekToEnd" -> bool
│ ├─ "datasize" -> double (占位)
│ ├─ "videosize" -> double (占位)
│ ├─ "audiosize" -> double (占位)
│ ├─ "lasttimestamp" -> double (占位)
│ ├─ "lastkeyframetimestamp" -> double (占位)
│ ├─ "lastkeyframelocation" -> double (占位)
│ └─ "keyframes" -> AMF_OBJECT (占位,稍后填充)
│
├─→ 结束对象
│ └─ "" + AMF_END_OF_OBJECT
│
├─→ 回写大小
│ ├─ 计算metadata_totalsize
│ ├─ 回写ArraySize
│ ├─ 回写DataSize
│ └─ 写入PreviousTagSize
│
└─→ 完成
6.2.3 延迟写入实现
由于某些字段的值在编码完成前未知,需要延迟写入:
c
// 1. 写入时记录偏移
flv->duration_offset = avio_tell(pb);
put_amf_double(pb, s->duration / AV_TIME_BASE); // 占位值
// 2. 在trailer中回写
avio_seek(pb, flv->duration_offset, SEEK_SET);
put_amf_double(pb, flv->duration / (double)1000);
6.3 flv_write_codec_header函数
flv_write_codec_header函数为每个流写入编解码器特定的序列头。
6.3.1 支持的编解码器
音频编解码器:
- AAC: Sequence Header包含AAC配置信息
- OPUS: Sequence Header包含OPUS配置信息
- FLAC: Sequence Header包含FLAC配置信息
- AC3/EAC3: Sequence Header包含AC3配置信息
- MP3: 不需要Sequence Header(多轨道除外)
视频编解码器:
- H.264: Sequence Header包含AVC配置(SPS/PPS)
- HEVC: Sequence Header包含HEVC配置(VPS/SPS/PPS)
- AV1: Sequence Header包含AV1配置
- VP9: Sequence Header包含VP9配置
- MPEG4: Sequence Header包含MPEG4配置
6.3.2 AAC Sequence Header生成
如果AAC流没有extradata,且启用了FLV_AAC_SEQ_HEADER_DETECT标志,编码器会自动生成:
c
static void flv_write_aac_header(AVFormatContext* s, AVCodecParameters* par)
{
if (!par->extradata_size && (flv->flags & FLV_AAC_SEQ_HEADER_DETECT)) {
PutBitContext pbc;
int samplerate_index;
int channels = par->ch_layout.nb_channels
- (par->ch_layout.nb_channels == 8 ? 1 : 0);
uint8_t data[2];
// 查找采样率索引
for (samplerate_index = 0; samplerate_index < 16; samplerate_index++)
if (par->sample_rate == ff_mpeg4audio_sample_rates[samplerate_index])
break;
// 生成AAC配置
init_put_bits(&pbc, data, sizeof(data));
put_bits(&pbc, 5, par->profile + 1); // profile (5 bits)
put_bits(&pbc, 4, samplerate_index); // sample rate index (4 bits)
put_bits(&pbc, 4, channels); // channels (4 bits)
put_bits(&pbc, 1, 0); // frame length - 1024 samples
put_bits(&pbc, 1, 0); // does not depend on core coder
put_bits(&pbc, 1, 0); // is not extension
flush_put_bits(&pbc);
avio_w8(pb, data[0]);
avio_w8(pb, data[1]);
}
avio_write(pb, par->extradata, par->extradata_size);
}
AAC配置字节结构:
Byte 0: Bit 7-3: AudioObjectType (profile + 1)
Bit 2-0: (高3位) SampleRateIndex
Byte 1: Bit 7-4: (低4位) SampleRateIndex
Bit 3-0: ChannelConfiguration
6.4 flv_write_metadata_packet函数
flv_write_metadata_packet函数为HEVC/AV1/VP9视频流写入颜色信息元数据。
6.4.1 Enhanced RTMP颜色信息
Enhanced RTMP规范定义了颜色信息的传输格式,用于HDR视频:
VideoTag (Extended Header)
TagType: 0x09
DataSize: N
Timestamp: DTS
Tag Data:
Byte 0: 0x80 | PacketTypeMetadata | FLV_FRAME_VIDEO_INFO_CMD
Byte 1-4: FourCC
Byte 5+: AMF Object "colorInfo"
"colorConfig": {
"transferCharacteristics": number (color_trc)
"matrixCoefficients": number (color_space)
"colorPrimaries": number (color_primaries)
}
"hdrCll": { // 如果存在
"maxFall": number
"maxCLL": number
}
"hdrMdcv": { // 如果存在
"redX", "redY": number
"greenX", "greenY": number
"blueX", "blueY": number
"whitePointX", "whitePointY": number
"maxLuminance": number
"minLuminance": number
}
6.4.2 函数实现
c
static void flv_write_metadata_packet(AVFormatContext *s, AVCodecParameters *par,
unsigned int ts, int stream_idx)
{
// 只处理HEVC/AV1/VP9
if (par->codec_id == AV_CODEC_ID_HEVC ||
par->codec_id == AV_CODEC_ID_AV1 ||
par->codec_id == AV_CODEC_ID_VP9) {
// 获取HDR元数据
AVContentLightMetadata *lightMetadata = NULL;
AVMasteringDisplayMetadata *displayMetadata = NULL;
// 写入VideoTag Header
avio_w8(pb, FLV_TAG_TYPE_VIDEO);
metadata_size_pos = avio_tell(pb);
avio_wb24(pb, 0 + flags_size);
put_timestamp(pb, ts);
avio_wb24(pb, flv->reserved);
// 写入Extended Header
avio_w8(pb, FLV_IS_EX_HEADER | PacketTypeMetadata | FLV_FRAME_VIDEO_INFO_CMD);
write_codec_fourcc(pb, par->codec_id);
// 写入colorInfo AMF对象
avio_w8(pb, AMF_DATA_TYPE_STRING);
put_amf_string(pb, "colorInfo");
avio_w8(pb, AMF_DATA_TYPE_OBJECT);
// 写入colorConfig
put_amf_string(pb, "colorConfig");
avio_w8(pb, AMF_DATA_TYPE_OBJECT);
if (par->color_trc != AVCOL_TRC_UNSPECIFIED)
// 写入transferCharacteristics
if (par->color_space != AVCOL_SPC_UNSPECIFIED)
// 写入matrixCoefficients
if (par->color_primaries != AVCOL_PRI_UNSPECIFIED)
// 写入colorPrimaries
// 写入HDR元数据(如果存在)
// ...
// 回写大小
total_size = avio_tell(pb) - metadata_size_pos - 10;
avio_seek(pb, metadata_size_pos, SEEK_SET);
avio_wb24(pb, total_size);
avio_skip(pb, total_size + 10 - 3);
avio_wb32(pb, total_size + 11);
}
}
元数据处理机制
7.1 元数据分类
FLV编码器处理的元数据分为三类:
- 基础元数据:视频/音频的基本参数(宽高、码率、采样率等)
- 用户元数据:通过AVFormatContext->metadata传入的自定义元数据
- 统计元数据:编码过程中统计的信息(文件大小、时长、关键帧信息等)
7.2 元数据过滤
编码器会过滤掉一些保留关键字,避免与系统元数据冲突:
c
if( !strcmp(tag->key, "width")
||!strcmp(tag->key, "height")
||!strcmp(tag->key, "videodatarate")
// ... 其他保留关键字
){
av_log(s, AV_LOG_DEBUG, "Ignoring metadata for %s\n", tag->key);
continue;
}
7.3 元数据更新
如果设置了AVSTREAM_EVENT_FLAG_METADATA_UPDATED标志,编码器会在写入数据包时重新写入元数据:
c
if (s->event_flags & AVSTREAM_EVENT_FLAG_METADATA_UPDATED) {
write_metadata(s, ts);
s->event_flags &= ~AVSTREAM_EVENT_FLAG_METADATA_UPDATED;
}
关键帧索引系统
8.1 索引的作用
关键帧索引允许播放器:
- 快速定位到任意时间点(seek操作)
- 计算播放进度
- 实现快进/快退功能
8.2 索引数据结构
索引以链表形式存储:
c
typedef struct FLVFileposition {
int64_t keyframe_position; // 文件位置(字节偏移)
double keyframe_timestamp; // 时间戳(秒)
struct FLVFileposition *next; // 下一个节点
} FLVFileposition;
8.3 索引构建流程
编码过程中(flv_write_packet)
│
└─→ 检测到关键帧
├─ 记录当前位置: cur_offset
├─ 记录时间戳: pkt->dts / 1000.0
└─ flv_append_keyframe_info()
├─ 分配FLVFileposition节点
├─ 设置keyframe_position和keyframe_timestamp
└─ 添加到链表尾部
编码结束时(flv_write_trailer)
│
├─→ 计算索引大小
│ └─ metadata_size = filepositions_count * 9 * 2 + ...
│
├─→ 移动文件数据
│ └─ shift_data()
│ ├─ 计算需要移动的数据大小
│ └─ ff_format_shift_data()
│ └─ 将keyframes_info_offset之后的数据后移
│
├─→ 更新元数据中的偏移引用
│ └─ 所有filepositions += keyframe_index_size
│
└─→ 写入索引数据
├─ "filepositions"数组
└─ "times"数组
8.4 shift_data函数详解
shift_data函数负责在文件中插入关键帧索引数据:
c
static int shift_data(AVFormatContext *s)
{
int64_t metadata_size = 0;
FLVContext *flv = s->priv_data;
// 计算索引大小
metadata_size = flv->filepositions_count * 9 * 2 + 10; // filepositions和times值
metadata_size += 2 + 13; // "filepositions"字符串
metadata_size += 2 + 5; // "times"字符串
metadata_size += 3; // 对象结束
flv->keyframe_index_size = metadata_size;
// 移动数据
ret = ff_format_shift_data(s, flv->keyframes_info_offset, metadata_size);
if (ret < 0)
return ret;
// 更新元数据Tag的大小
avio_seek(s->pb, flv->metadata_size_pos, SEEK_SET);
avio_wb24(s->pb, flv->metadata_totalsize + metadata_size);
// 更新PreviousTagSize
avio_seek(s->pb, flv->metadata_totalsize_pos + metadata_size, SEEK_SET);
avio_wb32(s->pb, flv->metadata_totalsize + 11 + metadata_size);
return 0;
}
大小计算说明:
- 每个关键帧位置:8字节(double)+ 1字节(AMF类型)= 9字节
- filepositions数组:filepositions_count * 9字节
- times数组:filepositions_count * 9字节
- 字符串和对象开销:约20字节
多轨道支持
9.1 多轨道概念
FLV格式支持在一个文件中包含多个视频轨道或多个音频轨道,这通过Enhanced RTMP扩展实现。
9.2 轨道索引映射
track_idx_map数组将流索引映射到轨道索引:
c
// 在flv_init中
for (i = 0; i < s->nb_streams; i++) {
switch (par->codec_type) {
case AVMEDIA_TYPE_VIDEO:
flv->track_idx_map[i] = video_ctr++;
break;
case AVMEDIA_TYPE_AUDIO:
flv->track_idx_map[i] = audio_ctr++;
break;
}
}
- 第一个视频流:track_idx = 0
- 第二个视频流:track_idx = 1
- 第一个音频流:track_idx = 0
- 第二个音频流:track_idx = 1
9.3 多轨道Tag格式
多轨道视频Tag:
Tag Data:
Byte 0: 0x80 | 0x06 | PacketType | FrameType
0x06 = Multitrack标志
Byte 1: 0x00 | PacketType
0x00 = MultitrackTypeOneTrack
Byte 2-5: FourCC
Byte 6: TrackIdx
Byte 7+: 视频数据
多轨道音频Tag:
Tag Data:
Byte 0: 0x90 | 0x05 | PacketType
0x90 = Extended Header
0x05 = AudioPacketTypeMultitrack
Byte 1: 0x00 | PacketType
0x00 = MultitrackTypeOneTrack
Byte 2-5: FourCC
Byte 6: TrackIdx
Byte 7+: 音频数据
9.4 多轨道编解码器限制
视频多轨道:仅支持VP8/VP9/AV1/H.264/HEVC
音频多轨道:仅支持AAC/MP3/OPUS/FLAC/AC3/EAC3
9.5 多声道音频支持
对于多声道音频(超过立体声),需要写入额外的多声道配置Tag:
c
if (par->codec_type == AVMEDIA_TYPE_AUDIO &&
(extended_flv ||
(av_channel_layout_compare(&par->ch_layout, &AV_CHANNEL_LAYOUT_STEREO) == 1 &&
av_channel_layout_compare(&par->ch_layout, &AV_CHANNEL_LAYOUT_MONO) == 1)))
flv_write_multichannel_header(s, par, ts, stream_index);
flv_write_multichannel_header函数写入多声道配置:
AudioTag (MultichannelConfig)
TagType: 0x08
DataSize: N + 5+
Tag Data:
Byte 0: 0x90 | AudioPacketTypeMultichannelConfig
Byte 1-4: FourCC
Byte 5: TrackIdx (如果多轨道)
Byte 6: ChannelOrder
Byte 7: ChannelCount
Byte 8+: ChannelMap (如果ChannelOrder == Native或Custom)
性能优化与最佳实践
10.1 内存管理
FLV编码器在初始化时分配固定大小的数组,避免运行时动态分配:
c
flv->last_ts = av_calloc(s->nb_streams, sizeof(*flv->last_ts));
flv->metadata_pkt_written = av_calloc(s->nb_streams, sizeof(*flv->metadata_pkt_written));
flv->track_idx_map = av_calloc(s->nb_streams, sizeof(*flv->track_idx_map));
关键帧索引使用链表结构,只在需要时分配节点。
10.2 文件I/O优化
- 批量写入 :使用
avio_write批量写入数据,减少系统调用 - 延迟写入:元数据字段使用延迟写入,避免多次seek
- 缓冲区管理:AVIOContext内部有缓冲区,减少实际I/O次数
10.3 数据格式转换优化
对于H.264/HEVC的Annex-B到MP4格式转换:
- 检查extradata格式:如果extradata已经是MP4格式(第一个字节为1),则不需要转换
- 原地转换:如果可能,尽量在原地修改数据,避免额外内存分配
- 批量处理:一次性处理整个packet,而不是逐个NALU处理
10.4 关键帧索引优化
- 延迟构建:索引在trailer阶段一次性构建,避免频繁的文件操作
- 大小预计算:提前计算索引大小,一次性分配空间
- 链表vs数组:使用链表存储索引,避免频繁的内存重分配
10.5 最佳实践建议
- 启用关键帧索引 :对于需要seek功能的场景,启用
add_keyframe_index标志 - 合理设置关键帧间隔:关键帧过多会增加文件大小,过少会影响seek性能
- 使用扩展格式:对于新编解码器(HEVC/AV1/VP9),使用扩展格式以获得更好的兼容性
- 元数据精简:避免写入过多不必要的元数据,减少文件大小
- 时间戳一致性:确保输入流的时间戳是单调递增的
总结
本文深入解析了FFmpeg中FLV编码器的实现原理,从文件格式到代码实现,从初始化到数据写入,涵盖了编码器的方方面面。
11.1 核心要点回顾
- FLV格式:Tag-based流式结构,支持音频、视频和脚本三种Tag类型
- 编码流程:初始化 → 写入头部 → 写入数据包 → 写入尾部
- 元数据处理:使用AMF格式,支持延迟写入和动态更新
- 关键帧索引:通过链表收集,在trailer阶段一次性写入
- 多轨道支持:通过Enhanced RTMP扩展实现,支持多视频/音频轨道
11.2 技术亮点
- 延迟写入机制:对于未知值的字段(duration、filesize),采用占位+回写的方式
- 数据格式转换:自动处理Annex-B到MP4格式的转换
- 扩展格式支持:支持Enhanced RTMP,兼容新编解码器
- 内存效率:使用链表和固定数组,平衡内存使用和性能
11.3 应用场景
FLV编码器广泛应用于:
- 视频直播推流(RTMP协议)
- 视频点播文件生成
- 视频转码和格式转换
- 流媒体服务器
11.4 未来展望
随着视频编码技术的发展,FLV格式也在不断演进:
- Enhanced RTMP规范持续更新
- 新编解码器支持(AV1、VP9等)
- HDR和色彩空间信息的完善
- 多轨道和多声道音频的增强
理解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