已经很久没有写过技术博客了,这段时间加入了新公司,主要时间花在熟悉新业务的技术上。而新的业务主要跟音视频相关,关于音视频的尝试在加入新公司之前,自己有做相关demo的尝试与学习,可以参看音视频相关学习demo。当然,那都是自己“想当然”学习的一些东西,虽然实际工作中并没有派上太大的用处,但让我对音视频相关的基础知识有了一定的概念,对后面的技术尝试做了铺垫。第一个技术挑战比较大的就是进行:视频抽帧,关于视频抽帧网上有很多很多文章进行讲解,但……我始终没有找到一个效率很高的解决方案。直到我遇见了 ffmpeg,仿佛打开了新世界的大门……

关于FFmpeg

刚接触 ffmpeg 时,我一脸懵逼,完全不知道该怎么做,也不知道在哪里开始进行学习,后来在雷霄骅大神的博客中渐渐找到了感觉,膜拜!不过雷神的博客代码是基于老版本的 ffmpeg api,推荐搭配官方example,先跑通雷声的博客,再对照官方的例子对进行api相关接口的修改。

当然,想要使用 ffmpeg编写代码之前,我们首先要做的是对 FFmpeg 进行so库编译,这一步也是难倒了众多的英雄好汉,引用FFmpeg so库编译作者的话:

为什么FFmpeg让人觉得很难搞?
我想主要是因为迈出第一步就很困难,连so库都编译不出来,后面的都是扯淡了。

参考FFmpeg so库编译文章能成功地打包出 ffmpeg.so,接下来就是添加在项目中运行。

踏上 FFmpeg 音视频之路

关于音视频等开发,无论是做特效渲染还是做视频播放,那么最重要也是最基本的步骤就是:音视频解码

众所周知的是视频是由一帧帧视频帧(图片)/音频帧编码组合而成

动画

视频解码要做的就是解码出视频文件中的每一帧,我们以:将视频转化为一帧帧的图片作为例进行学习。

FFmpeg 提取视频每一帧图像

在学习之前,我们思考一个问题:抛开 ffmpeg,如果让你去设计一个提取的代码,n你会怎么设计?

因为视频是以文件流的形式存在,我相信很多人一上来就能想到这样的结构:

1
2
3
4
5
while (!EOF) { //当文件流没有结束
Stream stream = getStream(); //获取一定区域的stream
Frame steam = getFrame(stream); //Stream转化为视频帧
Picture picture = decodeFrame(steam); //将视频帧转化为 .jpeg等格式图片
}

的确是这样的,这里是给出一份ffmpeg提取视频帧图片的核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AVFrame frame = av_frame_alloc(); 
while (true) {
if (av_read_frame(fmt_ctx, &avpkt) >= 0) { // Return the next frame of a stream.
if (avpkt.stream_index == video_stream_index) { //标识该AVPacket所属的视频/音频流。
avcodec_send_packet(codeCtx, &avpkt); //Supply raw packet data as input to a decoder.
while (avcodec_receive_frame(codeCtx, frame) == 0) { //Return decoded output data from a decoder.
snprintf(buf, sizeof(buf), "%s/frame-%d.jpg", out_filename, frame_count);
saveJpg(frame, buf);
}
frame_count++;
}
av_packet_unref(&avpkt);
} else {
LOGE("//Exit");
break;
}
}

上面的代码块就是 ffmpeg 进行视频解码最核心的逻辑了,主要的注释也贴在了代码上,完整代码请查看video_to_jpeg.cpp,查看完整的代码后,会感觉到很惊讶:为什么这么复杂?特别是前面的初始化操作。放心,ffmpeg就像一套组合拳,有固定不变的套路,写一次就足够了,了解了其中的流程,之后理解起来就会很容易了。

上面的代码我们还可以做一些其他处理,比如只获取关键帧、查找指定时间戳位置的帧、视频按2s一帧进行抽取、视频不保存为jpeg文件转化为Java的bitmap?

这些实现需求也都是基于上述核心模块进行修改:

如果想只获取关键帧,可以利用AVFrame对象的属性AVFrame->key_frame进行判断。

查找指定时间戳位置的帧:利用 av_seek_frame查找到指定帧时间最近的关键帧,然后依次进行编码,直到pts与目标时间相近

视频按2s一帧进行抽取:简单的操作可以去获取视频fps,比如视频25fps,可以使用一个计数器判断if(frame_count%25==0),这时候则是刚好1s。当然这样子性能不太好。如果需要追求性能,那么也可以利用av_seek_frame,查找目标时间附近,然后循环进行解码直到目标时间。

视频不保存为jpeg文件转化为Java的Bitmap:只需要对最终获取的 AVFrame做不一样的操作进行了,获取到对应的buffer,再利用jni调用构造 Java 的 bitmap 对象。

可以做的还有很多……

总结

提取视频图片这个功能只是 FFmpeg 强大功能的九牛一毛,需要探究的还有很多很多……

如果能跑起来 FFmpeg 最简单的例子,已经迈出了很大一步了,但如果要理解其中的原理,还需要更多的基础知识,以及像AVPacketAVFrameAVCodec ……每一个类的数据结构,以及实现都需要仔细研究。

自己在网上找到的 FFmpeg 相关的教程,以及自己想要去实现的功能的资源太少,很多东西都需要自己去摸索。有时候我总在怀疑:为什么这么基础且很实用的功能没有现成的轮子? 这可能也是现在音视频相关开发的现状吧,成熟可用的轮子相对而言较少,以及相关技术的分享可能不太好做。既然没有,那就靠自己一点点积累吧。

学习之路,任重而道远呐。