背景
- 基于ffmpeg实现录像功能,性能不理想,前后路摄像头视频码率相加只有28Mbps加上音频也只有4MB/s左右,使用class 10的sd卡 + 2秒 ringbuffer缓存的情况下,依然出现写卡不及时导致的丢帧现象,class 4 sd卡表现更差。
分析
- 使用dd命令测试class 10 SD卡在该平台上的读写速度,写速度能达到7~8MB/s,因此丢帧问题不是SD卡写性能不足导致的,是录像逻辑的问题。
- 通过测试定位到ffmpeg接口av_write_frame(写一帧数据)耗时异常,异常部分log如下:
01-01 00:01:13.064 139 310 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE/2022_01_01_000059_00.MOV]: av_write_frame take [19] ms
01-01 00:01:13.067 139 313 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/2022_01_01_000059_00_b.MOV]: av_write_frame take [23] ms
01-01 00:01:13.170 139 313 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/2022_01_01_000059_00_b.MOV]: av_write_frame take [102] ms
01-01 00:01:13.185 139 310 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE/2022_01_01_000059_00.MOV]: av_write_frame take [119] ms
01-01 00:01:13.187 139 313 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/2022_01_01_000059_00_b.MOV]: av_write_frame take [16] ms
01-01 00:01:13.190 139 310 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE/2022_01_01_000059_00.MOV]: av_write_frame take [4] ms
01-01 00:01:13.194 139 313 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/2022_01_01_000059_00_b.MOV]: av_write_frame take [6] ms
....
01-01 00:18:57.202 139 479 I write_packet@CviMuxer.cpp:577 [/mnt/sd/CARDV/MOVIE_b/2022_01_01_001833_00_b.MOV]: av_write_frame take [2249] ms
- 异常现象总结
- av_write_frame耗时不稳定,普遍耗时几毫秒,和预期缓存未满时写缓存(内存拷贝)耗时较少不符。
- 间隔性出现耗时100ms以上,时常出现200、300,甚至500ms以上,偶尔出现耗时1s ~ 2s,一帧I帧视频数据才100KB~200KB,耗时完全不符合预期。
ffmpeg 实现
- ffmpeg录像写文件操作的代码(libavformat/file.c)是调用的open,write,read,seek等系统接口,使用open接口打开文件是没有应用层缓存,因此需要ffmpeg自己实现缓存机制,在file_open中可以设置URLContext 的min_packet_size和max_packet_size来定义缓存大小,代码(libavformat/aviobuf.c)会根据定义的缓存大小申请内存创建缓存,av_write_frame时会先将数据写入该缓存,缓存满了或者主动调用avio_flush接口会调用write接口将缓存数据传递给linux内核。
文件缓存
- 定义一定大小的文件缓存可以减少系统接口write的调用频率,以及内存拷贝的次数,以提高性能。
- 缓存大小需要是内存页,文件系统块(Block),文件系统IO操作块(IO Block)大小的整倍数,以避免跨页操作
- 系统读写SD卡都是以块为单位,而不是一个字节一个字节的读写,因此缓存大小如果出现不满一块的余量,系统也要将一整块写入sd卡,并且为了保证存储数据的正确性,系统需要先从sd卡读取出该块中缓存未覆盖到的数据,补充到写入数据中,浪费了一定的性能。
- 不同文件系统或者同一个文件系统,不同SD卡的IO Block也可能不同,需要注意:
[root]/mnt/usb# stat xxx
File: xxx
Size: 49 Blocks: 128 IO Block: 65536 regular file
...
[root]/mnt/usb# stat b.txt
File: b.txt
Size: 6 Blocks: 64 IO Block: 32768 regular file
...
# stat testfile
File: `testfile'
Size: 102 Blocks: 8 IO Block: 4096 regular file
...
- 缓存大小需要根据实际情况进行调整
- 同步IO情况下,缓存大小也会造成一定的性能影响,需要参考数据量,写文件间隔等因素,例如:视频25fps,写文件间隔40ms,缓存的数据最好保证在40ms内传递给linux内核,耗时多了会阻塞下一帧,一些嵌入式平台会影响帧率,耗时少了会增加write的调用次数。
异常原因
- 现有代码设置了的ffmpeg缓存大小为32KB,理论上不应该出现问题。
- 分析自有代码和ffmpeg源码可知上面的异常现象中的间隔性耗时过长应该是缓存满了调用write导致的,av_write_frame普遍耗时几毫秒以及偶发性耗时过长是录像逻辑破坏了缓存机制出现。
- 缓存机制被破坏
- 为满足其它功能,我们修改了ffmpeg源码,在一些条件下会做seek和write操作,这种情况下就会导致缓存机制被破坏。
- MOV,MP4文件有固定的头部数据 ftyp 和wide box 再加上mdat box 的HEAD一共36个字节,ffmpeg写完这部分数据后会主动调用avio_flush 先将这部分数据写入SD卡,后续再按缓存大小写数据,虽然写的数据大小是满足块的整倍数,但是已经发生错位和跨页。
源码:libavformat/movenc.c
@@ -6744,7 +6744,7 @@ static int mov_write_header(AVFormatContext *s)
}
}
....
avio_flush(pb); //需要注释掉
if (mov->flags & FF_MOV_FLAG_ISML)
mov_write_isml_manifest(pb, mov, s);
...
修复结果
- 修复完后测试结果
01-01 08:03:54.641 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [4] ms
01-01 08:03:54.641 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.641 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.641 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.641 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.642 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.642 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.642 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.644 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [1] ms
01-01 08:03:54.644 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.644 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.644 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
01-01 08:03:54.644 139 329 I write_packet@CviMuxer.cpp:575 [/mnt/sd/CARDV/MOVIE_b/1970_01_01_080317_00_b.MOV]: av_write_frame take [0] ms
- 大部分情况下av_write_frame耗时1ms以下,缓存满时耗时会稍微多点,最高时也不超过20ms。