章节
- 问题
- 分析
- 解决方案
- 结果
1、问题
1.1 问题描述
基于原生 ffmpeg api 封装而成的 libvideo_util 库最终经解码->帧上绘图->编码、生成的视频上传至 rgw 之后,出现线上拖拽访问卡顿的问题。
如下图所示:
左边 为经过 libvideo_util 库处理生成的视频文件,可以看到加载非常慢,且没面加载数据量大约为 4KB左右。
右边 为经过 ffmpeg 命令行处理过的视频文件,拖拽访问无问题,即可以实现快速预加载。
命令行如下所示:
ffmpeg -i bad.mp4 -c copy good.mp4
如果左边的视频推送给用户观看,可想而知当用户拖拽浏览视频时,体验效果是非常差的。
2、分析
2.1 是什么导致了上述问题?
我将问题描述给了导师,他说昨天正好去听了视频播放原理的分享,分享中有提到:视频的存储在文件中的逻辑存储方式是 box 形式存在,这些 box 分类如下(mp4info.exe 工具):
ftyp、free、mdat(视频实质内容)、moov(视频meta 信息)
分享中阐述到:如果 视频的meta信息存储在视频文件尾部,那么浏览器通过网络传输协议加载远程资源服务器视频流时,浏览器不能迅速得到视频 meta信息(全局信息),这会导致浏览器不能很好的对视频进行预加载处理,所以就会出现拖拽访问卡顿的问题。
解决办法是在生成目标视频时,将metadata信息(moov)提前至视频第一帧的前面。
我在网上找到关于 短视频秒开 的优化原理,其中的一段话让我坚定 将 moov 数据提前至视频第一帧是可行的方案。如下图所示:
3、解决方案
代码片段如下所示:
3.1 VideoWriter->新增 AVDictionary *dict;
/// 输出流 视频
typedef struct {
AVFormatContext *ofmt_ctx;
AVPacket avpkt;
CodecContext *arr_codec_ctx;
int video_stream; //视频流的流下标
int audio_stream;
uint8_t is_init;
struct SwsContext *scxt;
AVDictionary *dict;
AVFrame *RGBFrame;
AVFrame *YUVFrame;
uint8_t *yuv_buff;
uint8_t *out_buff;
int outbuff_len;
int error_code;
char error_msg[128];
} VideoWriter;
3.2 打开输出流-> 设置 moov 信息前置
/// 打开要写入的视频文件
int VideoWriter_OpenVideo(void *writer, void *reader, const char *target_file,
int width, int height) {
VideoWriter *w = (VideoWriter *) writer;
VideoReader *r = (VideoReader *) reader;
AVCodecContext *video_dec_ctx = r->arr_codec_ctx[r->video_stream].ctx;
if (width <= 0 || height <= 0) {
width = video_dec_ctx->width;
height = video_dec_ctx->height;
}
w->error_code = avformat_alloc_output_context2(&(w->ofmt_ctx), NULL, NULL,
target_file);
if (w->error_code < 0) {
snprintf(w->error_msg, sizeof(w->error_msg),
"avformat_alloc_output_context2 fail");
return -1;
}
if (_open_output_file(r, w, width, height) < 0) {
return -1;
}
//Open output file
if (!(w->ofmt_ctx->flags & AVFMT_NOFILE)) {
w->error_code = avio_open(&(w->ofmt_ctx->pb), target_file,
AVIO_FLAG_WRITE);
if (w->error_code < 0) {
snprintf(w->error_msg, sizeof(w->error_msg), "avio_open fail");
return -7;
}
}
// 移动 moov 至第一帧前面
// _set_mov_moov_ahead(w);
av_dict_set(&(w->dict),"moveflags","rtphint+faststart",0);
//打印 dict 信息
// if (av_dict_count(w->dict) > 0) {
// printf("Using muxer settings:");
//
// AVDictionaryEntry *entry = NULL;
// while ((entry = av_dict_get(w->dict, "", entry,
// AV_DICT_IGNORE_SUFFIX)))
// printf("\n\t%s=%s", entry->key, entry->value);
//
// printf("\n");
// }
w->error_code = avformat_write_header(w->ofmt_ctx, &(w->dict));
if (w->error_code < 0) {
snprintf(w->error_msg, sizeof(w->error_msg),
"avformat_write_header fail");
return -7;
}
//AV_PIX_FMT_YUV420P
w->scxt = sws_getContext(width, height, r->pix_fmt, width, height,
video_dec_ctx->pix_fmt, SWS_POINT, NULL, NULL, NULL);
if (NULL == w->scxt) {
w->error_code = -1;
snprintf(w->error_msg, sizeof(w->error_msg), "sws_getContext fail");
return -1;
}
w->RGBFrame = av_frame_alloc();
w->YUVFrame = av_frame_alloc();
w->yuv_buff = (uint8_t *) malloc((width * height * 3));
w->outbuff_len = (width * height * 3);
w->out_buff = (uint8_t *) malloc(w->outbuff_len);
w->is_init = 0;
return 0;
}
注意:
av_dict_set(&(w->dict),"moveflags","rtphint+faststart",0);
要放置在 avformat_write_header() 之前
3.3 调用 avformat_write_header() 函数:
w->error_code = avformat_write_header(w->ofmt_ctx, &(w->dict));
if (w->error_code < 0) {
snprintf(w->error_msg, sizeof(w->error_msg),
"avformat_write_header fail");
return -7;
}
4、结果
新版的libvideo_util ffmpeg 封装库 实现了类似边下边播功能,视频可以实现快速预加载、拖拽访问无卡顿。