前言
对FFmpeg就不做过多介绍了,之前有篇文章是对Linux环境下编译FFmpeg库,有兴趣的也可以看下,Shell脚本编写与执行编译ffmpeg库,编译好之后的继承就不赘述了,有很多博客写的很详细,提一个注意的地方就是gradle版本
可能会影响集成是否成功(一直提示couldnt find libnative-lib.so
的话建议修改下gradle版本
)。
注:
不同版本的FFmpeg差异还是比较大的,本文以4.0.5
为例
流程
使用FFmpeg播放视频主要是对视频的解封装,解码,转换,绘制的一个过程。贴一下大致的流程图(牵扯一些视频封装的基本知识,需要的铜须自行百度吧,还是比较简单的~)
- 在对视频解码之前需要打开视频文件吧,也就是解封装,这里需要拿到一个比较重要的上下文
AVFormatContext
,
//拿到AVFormatContext上下文,用于获取文件中的视频流、音频流
AVFormatContext *avFormatContext = avformat_alloc_context();
//打开视频文件,第一个参数是上下文,第二个参数是路径支持本地路径和url地址,
// 第三个参数表示输入参数可以填空(表示自动,视频默认),第四个参数是一个字典,相当于一个HashMap,可以存放很多配置参数,比如超时时间等
int resultCode = avformat_open_input(&avFormatContext, path, NULL, &options);
//解析文件中的流,一般第一个流是视频流,也有可能是视频+字幕流
int code = avformat_find_stream_info(avFormatContext, NULL);
- 遍历流获取视频流
//遍历流
for (int i = 0; i < avFormatContext->nb_streams; ++i) {
//流的类型为视频流,codecpar为解码器
if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_stream_index = i;
break;
}
}
- 拿到视频流之后就是对视频流进行解码、转换和绘制。如果要进行解码,那么要有解码器。解码器需要开启
//获取解码器
AVCodec *avCodec = avcodec_find_decoder(codecpar->codec_id);
//解码器的上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
//将解码器参数copy到解码器上下文
avcodec_parameters_to_context(avCodecContext, codecpar);
//打开解码器
if(avcodec_open2(avCodecContext, avCodec, NULL)<0){
LOGE("打开解码器失败")
}
- 接着申请AVPacket和AVFrame,其中AVPacket的作用是:保存解码之前的数据和一些附加信息,如显示时间戳(pts)、解码时间戳(dts)、数据时长,所在媒体流的索引等;AVFrame的作用是:存放解码过后的数据。
//解码 获取yuv数据,FFmpeg中yuv数据是封装在AVPacket中的
//申请AVPacket
AVPacket *avPacket = (AVPacket *) av_malloc(sizeof(AVPacket)); //申请AVPacket
av_init_packet(avPacket);
//申请AVFrame
AVFrame *avFrame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧
AVFrame *rgb_frame = av_frame_alloc();//分配一个AVFrame结构体,指向存放转换成rgb后的帧
- 设置rgb_frame是一个缓存区域。
//缓存区
uint8_t *out_buffer= (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_RGBA,
avCodecContext->width,avCodecContext->height));
//与缓存区相关联,设置rgb_frame缓存区
avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,avCodecContext->width,avCodecContext->height);
- 在Native层绘制必须用到ANativeWindow,所以需要获取ANativeWindow。
//取到nativewindow
ANativeWindow *aNativeWindow=ANativeWindow_fromSurface(env,surface);
- 下面就是解码过程和绘制过程
- A解码
while (av_read_frame(avFormatContext, packet) >= 0) {
LOGE("解码 %d",avPacket->stream_index)
LOGE("VideoIndex %d",video_stream_index)
if(packet->stream_index==video_index){
LOGE("视频流解码")
avcodec_decode_video2(avCodecContext, frame, &frameCount, packet)
}
av_free_packet(packet);
}
- B绘制
if (frameCount) {
//设置底层窗体buffer缓冲区的大小
ANativeWindow_setBuffersGeometry(aNativeWindow,avCodecContext->width,avCodecContext->height,WINDOW_FORMAT_RGBA_8888);
//锁,防止其它线程操作
ANativeWindow_lock(aNativeWindow, &outBuffer, NULL);
//yuv数据转换成rgb数据
//转换为rgb格式
sws_scale(swsContext,(const uint8_t *const *)avFrame->data,avFrame->linesize,0,
avFrame->height,rgb_frame->data,
rgb_frame->linesize);
// rgb_frame是有画面数据
uint8_t *dst= (uint8_t *) outBuffer.bits;
//一行数据 拿到一行有多少个字节 RGBA
int destStride=outBuffer.stride*4;
//像素数据的首地址
uint8_t * src= rgb_frame->data[0];
// 实际内存一行数量
int srcStride = rgb_frame->linesize[0];
for (int i = 0; i < avCodecContext->height; ++i) {
//将rgb_frame中每一行的数据复制给nativewindow
memcpy(dst + i * destStride, src + i * srcStride, srcStride);
}
//解锁
ANativeWindow_unlockAndPost(aNativeWindow);
usleep(1000 * 16);
}
}
最后就是资源回收和Java层的逻辑了,这里就不贴了,感兴趣的看下完整代码吧~ 完整代码见:github