本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:FLV(Flash Video)是IT行业中广泛用于网络视频流传输的格式,由Adobe开发,适用于在线播放。”flv合成实现”指将H264视频码流与AAC音频码流按FLV文件结构进行封装(mux),生成可播放的FLV文件。该过程涉及解析音视频数据、转换NALU和ADTS头、构建FLV头部与标签、插入metadata等步骤。通过阅读如flvenc.cpp和flv.h等源码文件,可深入掌握C++环境下FLV封装的技术细节。本项目实战涵盖从码流处理到文件生成的全流程,适用于直播、点播等场景,帮助开发者理解音视频封装核心机制。

FLV文件格式与音视频封装技术深度解析

在流媒体技术飞速发展的今天,FLV(Flash Video)虽然不再是前端的绝对主角,但它作为RTMP推流和点播系统的核心容器之一,依然活跃于大量直播、安防、教育平台中。🔥 你有没有想过,当你打开一个“秒开”的直播页面时,背后那串连续不断的 0x08 、 0x09 数据包是如何精准对齐音画、稳定传输的?这一切的秘密,都藏在FLV这个看似简单却极为精巧的二进制结构里。

我们今天不走寻常路——不会一上来就甩一堆术语定义,而是从 一行13字节的十六进制数据 开始,带你一层层剥开FLV的神秘外衣,看看它是如何把H.264和AAC这两股“原始野性”的码流驯服成可播放、可跳转、可交互的成熟视频文件的。

// 典型FLV文件前13字节示例:
46 4C 56 01 05 00 00 00 09 00 00 00 00

这短短的一行代码,就是整个FLV世界的起点。别小看它,这里面藏着身份认证、版本控制、轨道声明、偏移定位……所有后续操作的基石都在这里定下基调。接下来,我们就以这段数据为锚点,深入探索FLV封装的每一个关键环节。


🧱 FLV整体结构:线性布局下的精密协作

FLV文件的整体布局非常简洁,几乎可以用一句话概括:

一个头部 + 一串标签序列 + 每个标签后跟一个大小记录字段

但正是这种极简设计,支撑起了数十年来无数实时流媒体系统的运行。它的核心哲学是“追加写入”(append-only),非常适合边采集边推送的场景,比如直播推流。

文件头:9字节的身份铭牌

FLV Header 固定为9字节,结构如下:

字段 长度 内容
Signature 3B 'F','L','V' → 0x46 0x4C 0x56
Version 1B 通常为 0x01
Flags 1B 指示音视频是否存在
DataOffset 4B 起始位置,固定为 0x00000009

其中最值得玩味的是 Flags 字段。虽然文档上说 bit6 表音频、bit5 表视频,但实际上由于历史原因, 真正的编码方式是低位表示 :

  • audio_flag = 0x04 (即 bit2)
  • video_flag = 0x01 (即 bit0)

所以如果你看到 Flags = 0x05 ,那就意味着这是一个包含音视频双轨的FLV文件。而 DataOffset = 9 则明确告诉解析器:“第一个Tag从第10个字节开始”。

💡 小贴士:有些开发者误以为可以自定义这个偏移量,结果导致播放器无法识别文件。记住,除非你在做非标扩展,否则必须严格遵守 DataOffset = 9 !

紧接着Header之后,还有一个固定的4字节字段: PreviousTagSize0 ,值恒为0。这是为了统一处理逻辑——每个Tag后面都有一个 PreviousTagSize ,那么第一个Tag前面自然也要有个“前驱尺寸”,设为0是最合理的做法。

于是整个文件开头就是这经典的 13字节组合 :

[FLV Header (9)] + [PreviousTagSize0 (4)]

之后才是真正的数据内容登场。


🔖 Tag机制:时间轴上的信息单元

如果说Header是身份证,那 Tag就是FLV文件里的公民个体 。每一个Tag代表一段独立的信息片段,它们按时间顺序排列,构成了一条清晰的时间线。

三大Tag类型详解

类型 值(Hex) 含义 使用频率
Audio 0x08 音频帧(AAC/MP3) ⭐⭐⭐⭐☆
Video 0x09 视频帧(H.264/H.265) ⭐⭐⭐⭐⭐
Script 0x12 元数据(如 onMetaData) ⭐⭐☆☆☆

每个Tag的结构也非常标准:

[Tag Header (11)] + [Body] + [PreviousTagSize (4)]

其中 Tag Header 占11字节 ,具体分布如下:

字段 大小 说明
TagType 1B 0x08 / 0x09 / 0x12
DataSize 3B Body部分长度(大端序)
Timestamp 3B 时间戳(毫秒)
TimestampExtended 1B 时间戳高位扩展
StreamID 3B 固定为0

这里的 Timestamp 是32位整数 ,由低3字节(Timestamp)和高1字节(Extended)拼接而成,单位为毫秒。例如,如果想表示 12345 ms ,则:

  • Timestamp = 0x3039 (低三字节)
  • Extended = 0x00 (最高位)

这样就能支持最长约49.7天的播放时间,足够覆盖绝大多数使用场景了。

更巧妙的是,每个Tag后面紧跟的 PreviousTagSize 字段(4字节),不仅用于验证完整性,还让播放器具备了 反向遍历能力 !也就是说,即使你从文件末尾开始读,也能一步步往前找到关键帧,实现seek功能。

🧠 这种设计思想至今仍影响着现代容器格式,比如MP4中的 moov box也可以放在末尾,方便边录制边上传。


🎞️ H.264码流预处理:从原始比特流到可用帧

现在我们进入重头戏——如何将摄像头或编码器输出的原始H.264码流封装进FLV?

NALU结构解析:起始码与Header

H.264码流本质上是一系列NALU(Network Abstraction Layer Unit)的集合,每个NALU前面都有一个起始码(Start Code):

  • 0x000001 (3字节)
  • 或 0x00000001 (4字节)

起始码的作用是防止码流中出现类似同步头的数据造成误判,属于典型的“防竞争码”设计。

紧随其后的1字节是 NALU Header ,格式如下:

Bit 7 Bits 6-5 Bits 4-0
F (forbidden_zero_bit) NRI Type
  • F :必须为0,否则数据损坏
  • NRI :重要性等级,SPS/PPS通常是3
  • Type :决定该NALU的内容类型

常见Type值包括:

Type 含义
1 非IDR图像数据
5 IDR帧(关键帧)
6 SEI补充信息
7 SPS(序列参数集)
8 PPS(图像参数集)
9 AUD(访问单元分隔符)

我们重点来看几个核心组件。

✅ SPS & PPS:解码器的启动钥匙

没有SPS和PPS,播放器根本不知道该怎么解码视频画面。它们包含了诸如分辨率、帧率、profile等全局参数。

但在FLV中,不能直接把这些NALU扔进去完事。必须先打包成 AVCDecoderConfigurationRecord 结构,并作为首个视频Tag写入,且 AVCPacketType = 0 。

这个结构体看起来复杂,其实也就几项关键字段:

struct AVCDecoderConfig {
    uint8_t config_version = 1;
    uint8_t profile = sps[1];
    uint8_t compat = sps[2];
    uint8_t level = sps[3];
    uint8_t length_size_minus_one = 3; // 表示每个NALU前有4字节长度
    uint8_t num_sps = 1;
    uint16_t sps_len;
    const uint8_t* sps_data;
    uint8_t num_pps = 1;
    uint16_t pps_len;
    const uint8_t* pps_data;
};

构造时要注意:
- lengthSizeMinusOne = 3 是推荐值,表示使用4字节长度前缀(而非起始码)
- 所有数值均采用大端序(Big Endian)
- 必须作为第一个视频Tag写入!

否则你会发现:文件能打开,但黑屏——因为解码器压根没初始化成功 😵‍💫

✅ AUD:被低估的帧边界守护者

AUD(Access Unit Delimiter)是一个可选但极其有用的NALU类型(Type=9)。它就像一堵墙,清楚地标记出“这一帧到此为止”。

虽然很多编码器默认不开AUD,但我强烈建议开启!尤其是在多路混流、精确seek、低延迟推流等场景下,AUD能极大提升帧边界的准确性。

举个例子:假设你正在做一个直播连麦系统,需要对两路视频进行帧级对齐。如果没有AUD,仅靠IDR判断帧起始,很容易出现错位;而有了AUD,每一帧的开始都清清楚楚,时间戳分配也更加精准。

我们可以用一个简单的状态机来处理:

enum State { WAITING_FOR_AUD, IN_ACCESS_UNIT };

void input_nalu(const Nalu& nalu, int64_t pts) {
    if (nalu.type == 9) {  // AUD
        if (state == IN_ACCESS_UNIT)
            emit_frame();  // 上一帧完成
        start_new_frame(pts);
    } else {
        collect_nalu(nalu);  // 收集到当前帧中
    }
}

这样就能确保每组NALU对应一个完整画面,避免跨帧污染。


🔊 AAC码流处理:ADTS头剥离与配置提取

音频方面,最常见的输入是带有ADTS头的AAC流。ADTS是一种专为传输设计的包装格式,但在FLV中并不需要它。

ADTS头结构剖析

一个完整的ADTS帧由 7或9字节头部 + AAC原始数据 构成:

字段 位置 说明
Sync Word 12 bits 0xFFF ,用于帧同步
MPEG Version 1 bit 0=MPEG-4, 1=MPEG-2
Layer 2 bits 恒为0
Protection Absent 1 bit 是否有CRC校验
Profile 2 bits 编码类型(LC为主)
Sampling Freq Index 4 bits 查表得采样率
Channel Config 3 bits 声道数(1=mono, 2=stereo)
Frame Length 13 bits 整个帧长度(含头)

通过解析这些字段,我们可以还原出音频的基本属性:

bool parse_adts_header(const uint8_t* data, AdtsHeader* out) {
    if ((data[0] & 0xFF) != 0xFF || (data[1] & 0xF0) != 0xF0)
        return false;

    out->profile = (data[2] >> 6) & 0x03;
    out->sampling_freq_idx = (data[2] >> 2) & 0x0F;
    out->channel_config = ((data[2] & 0x01) << 2) | (data[3] >> 6);
    out->protection_absent = (data[1] >> 1) & 0x01;
    out->frame_length = ((data[3] & 0x03) << 11) | (data[4] << 3) | (data[5] >> 5);

    return true;
}

一旦解析成功,就可以计算出有效负载偏移量:

uint8_t header_size = protection_absent ? 7 : 9;
const uint8_t* payload = adts_frame + header_size;
size_t payload_size = frame_length - header_size;

这部分纯AAC数据就可以用于FLV封装了。

构造AudioSpecificConfig

为了让播放器知道怎么解码AAC,我们需要在首帧之前发送一个特殊的 AudioSpecificConfig ,它是MPEG-4规定的描述结构。

构造方法很简单:

uint8_t asc[2];
asc[0] = (profile << 3) | (sampling_idx >> 1);
asc[1] = ((sampling_idx & 1) << 7) | (channel_config << 3);

然后把这个2字节的数据作为 AACPacketType = 0 的音频Tag写入,后续帧则使用 AACPacketType = 1 。

流程图如下:

graph LR
    A[ADTS Frame] --> B{Parse Header}
    B --> C[Extract ASC Params]
    C --> D[Build AudioSpecificConfig]
    D --> E[Write as AAC Sequence Header]
    B --> F[Strip ADTS Header]
    F --> G[Get Raw AAC Data]
    G --> H[Write as AAC Raw Data Packet]

搞定这一步,你的音频就已经准备好融入FLV大家庭啦 🎉


🔧 FLV封装实战:从零构建合法文件

好了,理论讲完,咱们动手!

初始化FlvMuxer类

我们定义一个简单的C++类来管理整个封装过程:

class FlvMuxer {
public:
    bool Open(const char* path);
    void WriteVideoFrame(const uint8_t* data, size_t len, uint32_t pts, bool is_keyframe);
    void WriteAudioFrame(const uint8_t* data, size_t len, uint32_t pts);
    void WriteMetadata(double duration, int width, int height, double fps);
    void Finalize();

private:
    FILE* fp_;
    uint32_t previous_tag_size_ = 0;
    bool has_written_metadata_ = false;
};
打开文件并写Header
bool FlvMuxer::Open(const char* path) {
    fp_ = fopen(path, "wb");
    if (!fp_) return false;

    // 写FLV Header
    uint8_t header[9] = {'F', 'L', 'V', 1, 5, 0, 0, 0, 9};
    fwrite(header, 1, 9, fp_);

    // 写第一个PreviousTagSize (0)
    uint32_t zero = 0;
    fwrite(&zero, 4, 1, fp_);

    return true;
}

注意这里 Flags = 5 表示音视频都有。

写视频帧
void FlvMuxer::WriteVideoFrame(...) {
    TagHeader tag_hdr{};
    tag_hdr.tag_type = 0x09;
    tag_hdr.data_size = len + 5;  // 加上AVC Tag Header
    tag_hdr.timestamp = pts;
    tag_hdr.timestamp_extended = (pts >> 24) & 0xFF;

    AvcTagHeader avc_hdr{};
    avc_hdr.frame_type = is_keyframe ? 1 : 2;
    avc_hdr.codec_id = 7;  // H.264
    avc_hdr.avc_packet_type = 1;  // NALU
    avc_hdr.composition_time = 0;

    fwrite(&tag_hdr, 1, 11, fp_);
    fwrite(&avc_hdr, 1, 5, fp_);
    fwrite(data, 1, len, fp_);

    uint32_t current_size = 11 + 5 + len;
    fwrite(&current_size, 4, 1, fp_);  // PreviousTagSize

    previous_tag_size_ = current_size;
}

⚠️ 注意:如果是SPS/PPS,要设置 avc_packet_type = 0 并单独封装成 AVCDecoderConfigurationRecord 。

写音频帧
void FlvMuxer::WriteAudioFrame(...) {
    TagHeader tag_hdr{};
    tag_hdr.tag_type = 0x08;
    tag_hdr.data_size = len + 2;  // SoundInfo + AACPacketType
    tag_hdr.timestamp = pts;

    fwrite(&tag_hdr, 1, 11, fp_);

    uint8_t sound_info = (10 << 4) | (3 << 2) | (1 << 1) | 1;  // AAC, 44kHz, 16bit, stereo
    uint8_t aac_packet_type = 1;  // raw data
    fwrite(&sound_info, 1, 1, fp_);
    fwrite(&aac_packet_type, 1, 1, fp_);
    fwrite(data, 1, len, fp_);

    uint32_t current_size = 11 + 2 + len;
    fwrite(&current_size, 4, 1, fp_);
}
写元数据(onMetaData)

使用AMF0编码写入JSON-like结构:

void FlvMuxer::WriteMetadata(...) {
    std::vector<uint8_t> body;
    append_amf_string(body, "onMetaData");
    append_amf_object_start(body);
    append_amf_named_number(body, "duration", duration);
    append_amf_named_number(body, "width", width);
    append_amf_named_number(body, "height", height);
    append_amf_named_number(body, "framerate", fps);
    append_amf_object_end(body);

    TagHeader hdr{};
    hdr.tag_type = 0x12;
    hdr.data_size = body.size();
    hdr.timestamp = 0;

    fwrite(&hdr, 1, 11, fp_);
    fwrite(body.data(), 1, body.size(), fp_);

    uint32_t sz = 11 + body.size();
    fwrite(&sz, 4, 1, fp_);
}

AMF0编码虽然古老,但在Flash时代可是家常便饭。现在依然有不少播放器依赖它获取基础信息。


⏱️ 音视频同步:PTS驱动的时间轴模型

音视频不同步?多半是时间戳搞错了!

FLV要求所有Tag必须按时间戳升序排列。这意味着即便原始码流中存在B帧导致DTS < PTS,我们也得按显示顺序重新排序。

时间戳转换公式

原始时间戳通常是基于“time_base”的整数,需换算为毫秒:

uint32_t to_ms(int64_t pts, AVRational time_base) {
    return static_cast<uint32_t>((pts * time_base.num * 1000LL) / time_base.den);
}

例如:
- 视频 time_base = 1/90000,PTS=4500 → 50ms
- 音频 time_base = 1/44100,每帧1024样本 → ~23.22ms

然后在主循环中比较音视频PTS,谁小写谁:

while (has_video || has_audio) {
    if (video_pts <= audio_pts && has_video)
        write_video(...);
    else if (has_audio)
        write_audio(...);
}

这样才能保证音画对齐,不会出现“嘴型滞后”或“回声错乱”。


🗺️ 关键帧索引与随机访问优化

为了让用户能拖动进度条,我们必须提供关键帧列表。

做法是在文件末尾再写一个脚本Tag,包含所有IDR帧的时间戳和文件偏移:

{
  "keyframes": {
    "times": [0, 2.033, 4.067],
    "filepositions": [13, 10452, 21876]
  }
}

这样播放器就能快速定位最近的关键帧,实现毫秒级seek。

当然,也可以选择在开头就预留空间,结尾再回填duration和keyframes,适合录制完成后生成的文件。


🛠️ 测试验证:确保兼容性万无一失

生成完FLV后,别急着庆祝,先跑一遍测试:

推荐工具清单

工具 命令 目的
FFmpeg ffmpeg -i test.flv -f null - 检查是否报错
VLC 图形界面打开 看能否正常播放
mpv mpv --demuxer-lavf-format=flv test.flv 调试解析细节
Chrome MSE + flv.js Web端兼容性实测

常见问题排查表

现象 可能原因 解决方案
文件打不开 Header错误 检查前13字节
黑屏无画面 SPS/PPS未正确封装 检查AVCPacketType是否为0
无声 AAC sequence header缺失 确保首帧写了ASC
播放卡顿 起始码未去除 输入应为纯NALU
无法seek 无关键帧索引 添加keyframes元数据
音画不同步 时间戳基准不一致 统一使用PTS并归一化

只要满足以下条件,基本就能通吃99%的播放环境:

✅ Header合法
✅ metadata位于起始附近
✅ 首个视频帧为IDR+SPS/PPS
✅ 所有Tag按时间戳递增
✅ PreviousTagSize正确回填


💡 总结与思考:为什么FLV仍未退出舞台?

尽管HTML5不再原生支持FLV,但通过 flv.js + MSE 技术栈,它依然能在浏览器中流畅播放。更重要的是,在 低延迟直播、CDN边缘推流、安防监控回放 等领域,FLV因其结构简单、易于解析、兼容性强等特点,仍然是首选容器之一。

它的设计理念—— 线性存储、标签化组织、时间戳驱动 ——甚至影响了后来的fMP4和CMAF的发展。

所以,掌握FLV不仅仅是学一个旧格式,更是理解流媒体底层逻辑的一把钥匙🔑。当你真正读懂那一行行十六进制数据背后的含义时,你会发现:原来所谓的“协议”,不过是人类给机器写的诗罢了 🌟


“复杂的事情简单做,简单的事情重复做,重复的事情用心做。”
—— 在音视频的世界里,每一次成功的封装,都是对细节的敬畏。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:FLV(Flash Video)是IT行业中广泛用于网络视频流传输的格式,由Adobe开发,适用于在线播放。”flv合成实现”指将H264视频码流与AAC音频码流按FLV文件结构进行封装(mux),生成可播放的FLV文件。该过程涉及解析音视频数据、转换NALU和ADTS头、构建FLV头部与标签、插入metadata等步骤。通过阅读如flvenc.cpp和flv.h等源码文件,可深入掌握C++环境下FLV封装的技术细节。本项目实战涵盖从码流处理到文件生成的全流程,适用于直播、点播等场景,帮助开发者理解音视频封装核心机制。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐