ITPUB

一条命令让CDN视频带宽成本降低90%!!!

  • 1 背景
  • 2 现象定位
    • 2.1 问题流量定位
    • 2.2 流量使用验证
  • 3 问题探究
    • 3.1 MP4 文件结构
    • 3.2 浏览器播放流程
    • 3.3 源码分析
    • 3.4 测试验证
  • 4 分析原因
    • 4.1 为什么一开始会出现3个206请求?
    • 4.2 为什么会发起多个206请求,且Range范围来回跳动?
    • 4.3 为什么使用ffmpeg处理视频后能解决这个问题?
  • 5 总结
  • 6 参考

1 背景

一个春暖花开的午后,发生了一个诡异的现象:

IT同学:哪个域名流量这么大,太占用公司办公网资源了!

某业务:想问下这个问题需要怎么解决呢,我们已经遇到好几例了,上次有个店员一天消耗了60G,这个店员130G 

我:收到CDN带宽告警,🤔哪个cdn域名用了这么多带宽啊!

2 现象定位

2.1 问题流量定位

通过CDN流量分析,定位到top流量消耗都是几个MP4视频。 点击流量最高的视频链接分析了下,是一个大小1G,视频时长约30分钟的MP4视频

于是找业务同学确认:

我:这个视频是做什么用的啊?

业务同学:这是一个学习培训视频,推送给了2500人学习观看。

我:嗯?不对啊,CDN监控显示花费流量120TB,如果2500人完播看完一个视频,正常应该是 2500x1g =2.5TB,这多出的50倍流量怎么来的?

2.2 流量使用验证

我通过谷歌浏览器访问了业务上传的另一个视频(视频大小500M时长40分钟),观看视频并开启F12分析下流量使用

Image
Image

完播居然花了8G,发送了3700个206请求!!!!

现象:

  • 开始播放后,会出现3个206请求。
  • 第三个请求发完后,浏览器随时间发起多个206请求
  • 请求来来回回Range
我:这不行啊,我赶紧ai、百度、google各种搜索
发现通过命令ffmpeg处理视频可以解决这个问题

ffmpeg -i bad.mp4 -c copy -movflags faststart good.mp4
  • 将新视频上传,f12分析视频
  • 206请求只有一个了
  • 且多206请求现象消失
  • 完播视频流量消耗=原视频大小

ffmpeg能解决该问题,但长视频还是将其转为流媒体格式,推动业务改进后流量监控如下图

Image

3 问题探究

四个问题

  • 为什么一开始会出现3个206请求?
  • 3个206请求后,为什么会发起多个206请求?
  • 为什么请求头Range会来回跳跃?
  • 为什么使用ffmpeg处理视频后能解决这个问题?

要分析该奇异现象发生原因就得知道整个播放流程是怎么运作的。

3.1 MP4 文件结构

MP4 文件由多个 Box(盒子)组成,主要包括:

MP4 文件结构:
┌─────────────────────────────────────┐
│ ftyp (文件类型)                      │  ← 文件开头
├─────────────────────────────────────┤
│ mdat
─────────────────────────────────────┤
│ moov (元数据)                        │  ← 可能在开头或结尾
│  ├─ mvhd (文件头信息)                │
│  ├─ trak (视频轨道)                  │
│  │   ├─ stco (Chunk偏移表)          │  ← 记录数据位置
│  │   ├─ stsz (样本大小表)            │  ← 记录每帧大小
│  │   └─ stsc (样本到Chunk映射)       │  ← 记录帧的分布
│  └─ trak (音频轨道)                  │
│      ├─ stco (Chunk偏移表)          │
│      ├─ stsz (样本大小表)            │
│      └─ stsc (样本到Chunk映射)    │
└─────────────────────────────────────┘

关键点:

  • moov box 包含了所有的索引信息(类似目录)
  • mdat box 包含了实际的音视频数据
  • 播放器必须先读取 moov 才能知道数据在哪里

3.2 浏览器播放流程

Image

从Media信息得知Chrome 浏览器使用 FFmpegDemuxer 来解析读取 MP4 文件,参考谷歌浏览器源码,播放流程如下:

Image

3.3 源码分析

mov_find_next_sample(这一个函数返回的是下一个样本位置):

static AVIndexEntry *mov_find_next_sample(AVFormatContext *s, AVStream **st)
{
..........
..........
    // 遍历所有流,找到最佳样本
for (i = 0; i < s->nb_streams; i++) {
        AVIndexEntry *current_sample = &index_entries[current_index];
        int64_t dts = current_sample->timestamp;

        // 
if (dtsdiff <= AV_TIME_BASE && current_sample->pos < sample->pos) {
            // DTS 差 ≤ 1 秒,选择位置靠前的(减少 seek)
            sample = current_sample;
        } elseif (dtsdiff > AV_TIME_BASE && dts < best_dts) {
            // DTS 差 > 1 秒,选择 DTS 最小的(保证顺序)
            sample = current_sample;
            ..........
..........
        }
    }
return sample;
}
}


在mov_find_next_sample函数中还有一个开关interleaved_read:

在解复用器层对多个轨道的包进行交错。对于交错不良的文件,这可以防止由于不同轨道之间包的大间隙而引起的播放问题,因为MOV/MP4文件对包的放置没有要求。然而,对于非常交错不良的文件,这可能会导致过多的寻道操作,因为需要在轨道之间寻道,所以禁用此功能可能会防止I/O问题,但代价是可能影响播放。

理想情况下,一个交错良好的 MP4 文件 会将音频和视频数据包按照时间顺序交替排列:

[视频包1] [音频包1] [视频包2] [音频包2] [视频包3] [音频包3] ...

这种交错存储方式的主要优点包括:

  • 减少磁盘访问次数:播放器能连续读取音视频数据,提高读取效率。
  • 降低播放缓冲区需求:播放时能够平滑加载音视频数据。
  • 支持渐进式下载和播放:支持按需下载,提升流媒体性能。

然而,如果音视频数据没有交错,而是集中存储,比如:

[视频包1] [视频包2] [视频包3] ... [音频包1] [音频包2] [音频包3] ...

会带来以下问题:

  • 频繁寻址:播放器需要在视频和音频数据之间跳转,增加磁盘访问次数。
  • 大量 HTTP Range 请求:每次跳转都需要发起新的 HTTP 请求,降低性能。
  • 播放不稳定:频繁请求数据会导致卡顿和延迟,影响用户体验。

3.4 测试验证

由于谷歌浏览器使用的是 FFmpegDemuxer 解码并读取视频信息,其底层依赖于 FFmpeg 代码实现,只是在上层进行了浏览器的缓冲/预读封装。

测试视频:longbad.mp4(该视频存在较差的音视频交错问题)

测试工具:FFmpeg(模拟谷歌浏览器的解码处理过程)

本次测试旨在评估 interleaved_read 参数对视频播放的影响,并借助ffmpeg工具分析视频多206的问题。

交错读取(Interleaved Read):默认情况下,FFmpeg 会将音视频数据交错存储,Interleaved为1;

ffmpeg  -i https://xxxx/longbad.mp4 -ss 60 -t 5.0 -y output.mp4 -loglevel trace 2>&1 | grep "Range"

-i https://...:从远程服务器读取输入文件
-ss 60:从视频的第60秒开始
-t 5.0:截取5秒的片段
-y:覆盖输出文件
output.mp4:输出文件名

通过wc -l统计发现请求了5078次!,看5s的视频请求了5078次。

结合cdn日志和longbad视频的音视频轨道分析Image

  • Range来回跳动,在60s时刻从5967030跳到46622680
  • 相同时间范围,音频offset和视频offset距离很远
非交错读取(Non-interleaved Read):通过设置 -interleaved_read 0,FFmpeg 会将音频和视频数据分开读取。

ffmpeg -interleaved_read 0 -i https://xxxx/longbad.mp4 -ss 60 -t 5.0 -y output.mp4 -loglevel trace 2>&1 | grep "Range"

通过wc -l统计发现请求了3只请求了三次

4 分析原因

4.1 为什么一开始会出现3个206请求?

原因:moov box 在文件末尾

moov存着视频的索引信息,详细看MP4 文件结构

正常的 MP4 文件播放流程:

读取 ftyp+moov → 在头部直接读mdat ----一个206

但问题视频的 moov 在文件末尾

Image

导致:

1. 读取 ftyp和moov →  不在头部啊去尾部查    -----第一个206
2. 读取尾部moov    →                     -----第二个206
3. 开始播放视频    →                      -----第三个206 

4.2 为什么会发起多个206请求,且Range范围来回跳动?

原因:视频文件交错不良 + FFmpeg 的读取策略

交错不良的文件布局

通过获取视频文件offset的信息制作了一个图

音视频物理位置交错图情况:
X轴 = Packet在文件中的物理顺序(按pos(offset)排序后的索引)
Y轴 = Stream类型(0=视频,1=音频)
每个条形 = 一个packet
颜色 = 红色=视频packet,青色=音频packet
可以看到有一大片音频在前边
Image
[音频包1] [音频包2] [音频包3] ...  ... [视频包1] [视频包2] [视频包3]

FFmpeg 的读取策略导致来回跳跃

mov_find_next_sample() 的选择逻辑

  • 时间差 ≤ 1 秒时,优先选择位置靠前的帧
  • 但交错不良导致"位置靠前"的帧距离offset很远
举例
音视频两个offset
A1 下一个V0

选择策略
时间轴(微秒):
0        10000      20000      30000      40000
├─────────┼──────────┼──────────┼──────────┤
│                    │          │
│                    A1         V0
│                 (23219)    (33333)
│                    │          │
│                    └──10114──┘
│                    (差值 < 1秒)
│
A0 (0)
V0 (0)

当视频帧之间时间<1秒 会选择最小的offset( pos )
这就导致交错不良的文件,DTS 差小但文件位置差大,然后来回请求

4.3 为什么使用ffmpeg处理视频后能解决这个问题?

原因:fmpeg对视频处理后,会对音视频的位置进行重新编排(流媒体除外),底层默认处理

处理视频文件后的音视频交错图Image

ffmpeg -i bad.mp4 -c copy -movflags faststart good.mp4
-i 输入指定文件
-c -copy  流拷贝
- movflags faststart 将视频moov提提前


ffmpeg会对其物理位置进行重新编排!!!
av_interleaved_write_frame() (libavformat/mux.c)  
    ↓
ff_interleave_packet_per_dts() (libavformat/mux.c) 
    ↓

static int interleave_compare_dts(AVFormatContext *s,
                                  const AVPacket *next,
                                  const AVPacket *pkt)
{
    AVStream *st  = s->streams[pkt->stream_index];
    AVStream *st2 = s->streams[next->stream_index];

    // 1. 比较 DTS(转换到统一时间基)
    int comp = av_compare_ts(next->dts, st2->time_base,
                            pkt->dts, st->time_base);

    // 2. 处理音频预加载(audio_preload)
if (s->audio_preload) {
        int preload  = st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
        int preload2 = st2->codecpar->codec_type == AVMEDIA_TYPE_AUDIO;
if (preload != preload2) {
            // 音频包提前一点时间
            int64_t ts = av_rescale_q(pkt->dts, st->time_base, AV_TIME_BASE_Q)
                       - preload * s->audio_preload;
            int64_t ts2 = av_rescale_q(next->dts, st2->time_base, AV_TIME_BASE_Q)
                        - preload2 * s->audio_preload;
            comp = (ts2 > ts) - (ts2 < ts);
        }
    }

    // 3. DTS 相同时,按流索引排序
if (comp == 0)
return pkt->stream_index < next->stream_index;

return comp > 0;
}

5 总结

    流媒体格式(如HLS)在带宽使用和播放流畅度上更优,但改造成本高。MP4格式因兼容性强和成本较低,仍是主流选择。综合考虑使用场景和成本,我们选择:
  • 短视频统一使用 FFmpeg 处理,确保 moov box 在文件开头,优化音视频交错,避免频繁的 Range 请求。
  • 长视频采用 HLS 等流媒体格式,按需加载视频片段,提升播放流畅度和带宽利用率。

6 参考资料

[1]

The investigation of excessive FFmpeg requests: https://blog.dreamfever.me/posts/2024-06-09-poor-performance-of-ffmpeg-i-url/#reference

[2]

endless canceled http requests when playing an mp4 in a <video> tag: https://issues.chromium.org/issues/40292515

[3]

ChromiumSrouce: https://source.chromium.org/chromium/chromium/src/+/main:third_party/ffmpeg/libavformat/mov.c;l=11048;bpv=1;bpt=1

[4]

FFmpegSoure: https://github.com/FFmpeg/FFmpeg/blob/94f2274a8b61438572f0873ccf430e55ce0e0e2b/libavformat/mov.c#L9767-L9795

关于作者 邹泉安,转转运维部。