在上一篇中,我们讲解了在Android平台上如何使用FFmpeg进行纯视频的解码和播放的,文章链接:FFmpeg_Android纯视频播放demo--基于旧接口 。本文在此基础上,修改为FFmpeg的解码新接口来进行讲解。
目前市面上两套接口都有人在用,不过推荐还是使用新接口,因为旧接口以后可能会慢慢淘汰,且新接口会兼容旧接口。
一、FFmpeg视频解码新接口流程图
FFmpeg视频的新接口的解码流程图如下,借用网上某位大佬的图解:
二、新接口解码调用流程
新接口与旧接口在很多初始化相关部分是大同小异的,主要的区别是在真正解码的那一步,采用了发送/接收这样的一种方式进行解码。
1、注册各大组件
这一步是ffmpeg的任何程序的第一步都是需要先注册ffmpeg相关的各大组件的:
//注册各大组件
av_register_all();
2、打开播放源并获取相关上下文
在解码之前我们得获取里面的内容,这一步就是打开地址并且获取里面的内容。其中avFormatContext是内容的一个上下文。
并使用avformat_open_input打开播放源,inputPath为输入的地址,可以是视频文件,也可以是网络视频流。然后使用avformat_find_stream_info从获取的内容中寻找相关流。
AVFormatContext *avFormatContext = avformat_alloc_context(); //获取上下文
//打开视频地址并获取里面的内容(解封装)
if (avformat_open_input(&avFormatContext, inputPath, NULL, NULL) < 0) {
LOGE("打开视频失败")
return;
}
if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
LOGE("获取内容失败")
return;
}
3、寻找视频流
我们在上面已经获取了内容,但是在一个音视频中包括了音频流,视频流和字幕流,所以在所有的内容当中,我们应当找出相对应的视频流。
这一步其实可以跟旧接口一样的写法,也可以按照下面这种写法:
//获取视频的编码信息
AVCodecParameters *origin_par = NULL;
int mVideoStreamIdx = -1;
mVideoStreamIdx = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (mVideoStreamIdx < 0) {
av_log(NULL, AV_LOG_ERROR, "Can't find video stream in input file\n");
return;
}
LOGE("成功找到视频流")
4、获取并打开解码器
这一步与旧接口类似,但是又有所不同,不过没有试过直接用旧接口是否可行。不过主要还多了一步avcodec_parameters_to_context去初始化解码器,否则解析avi封装的mpeg4视频没问题但是解析MP4封装的mpeg4视频会报错。新的流程如下:
// 寻找解码器 {start
AVCodec *mVcodec = NULL;
AVCodecContext *mAvContext = NULL;
mVcodec = avcodec_find_decoder(origin_par->codec_id);
mAvContext = avcodec_alloc_context3(mVcodec);
if (!mVcodec || !mAvContext) {
return;
}
//不初始化解码器context会导致MP4封装的mpeg4码流解码失败
int ret = avcodec_parameters_to_context(mAvContext, origin_par);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error initializing the decoder context.\n");
}
// 打开解码器
if (avcodec_open2(mAvContext, mVcodec, NULL) != 0){
LOGE("打开失败")
return;
}
LOGE("解码器打开成功")
// 寻找解码器 end}
5、申请AVPacket和AVFrame以及相关设置
这一步与新旧接口无关,所以与旧接口方法一致:
申请AVPacket和AVFrame,其中AVPacket的作用是:保存解码之前的数据和一些附加信息,如显示时间戳(pts)、解码时间戳(dts)、数据时长,所在媒体流的索引等;AVFrame的作用是:存放解码过后的数据。
//申请AVPacket
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
av_init_packet(packet);
//申请AVFrame
AVFrame *frame = 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,
mAvContext->width,mAvContext->height));
//与缓存区相关联,设置rgb_frame缓存区
avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,mAvContext->width,mAvContext->height);
SwsContext* swsContext = sws_getContext(mAvContext->width,mAvContext->height,mAvContext->pix_fmt,
mAvContext->width,mAvContext->height,AV_PIX_FMT_RGBA,
SWS_BICUBIC,NULL,NULL,NULL);
6、设置渲染绘制相关代码
这一步与新旧接口无关,主要是与Android平台操作相关,使用原生绘制,即是说需要ANativeWindow,与java层相呼应。
//取到nativewindow
ANativeWindow *nativeWindow=ANativeWindow_fromSurface(env,surface);
if(nativeWindow==0){
LOGE("nativewindow取到失败")
return;
}
//视频缓冲区
ANativeWindow_Buffer native_outBuffer;
7、开始解码
接下来就可以开始解码,解码新接口最大的区别就是改为使用发送/接收的方式进行解码流程的控制,如下是解码的核心段代码:
// 发送待解码包
int result = avcodec_send_packet(mAvContext, packet);
av_packet_unref(packet);
if (result < 0) {
av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
continue;
}
// 接收解码数据
while (result >= 0) {
result = avcodec_receive_frame(mAvContext, frame);
if (result == AVERROR_EOF)
break;
else if (result == AVERROR(EAGAIN)) {
result = 0;
break;
} else if (result < 0) {
av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
av_frame_unref(frame);
break;
}
av_frame_unref(frame);
}
8、解码、转换、渲染
第7步是解码的核心代码,但是实际上,我们实际代码中一般是边解码,边渲染绘制显示,如下完整解码渲染代码:
while(1) {
int ret = av_read_frame(avFormatContext, packet);
if (ret != 0) {
av_strerror(ret, buf, sizeof(buf));
LOGE("--%s--\n", buf);
av_packet_unref(packet);
break;
}
if (ret >= 0 && packet->stream_index != mVideoStreamIdx) {
av_packet_unref(packet);
continue;
}
{
// 发送待解码包
int result = avcodec_send_packet(mAvContext, packet);
av_packet_unref(packet);
if (result < 0) {
av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
continue;
}
// 接收解码数据
while (result >= 0) {
result = avcodec_receive_frame(mAvContext, frame);
if (result == AVERROR_EOF)
break;
else if (result == AVERROR(EAGAIN)) {
result = 0;
break;
} else if (result < 0) {
av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
av_frame_unref(frame);
break;
}
LOGE("转换并绘制")
//绘制之前配置nativewindow
ANativeWindow_setBuffersGeometry(nativeWindow, mAvContext->width,
mAvContext->height, WINDOW_FORMAT_RGBA_8888);
//上锁
ANativeWindow_lock(nativeWindow, &native_outBuffer, NULL);
//转换为rgb格式
sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0,
frame->height, rgb_frame->data,
rgb_frame->linesize);
// rgb_frame是有画面数据
uint8_t *dst = (uint8_t *) native_outBuffer.bits;
//拿到一行有多少个字节 RGBA
int destStride = native_outBuffer.stride * 4;
//像素数据的首地址
uint8_t *src = rgb_frame->data[0];
//实际内存一行数量
int srcStride = rgb_frame->linesize[0];
//int i=0;
for (int i = 0; i < mAvContext->height; ++i) {
//将rgb_frame中每一行的数据复制给nativewindow
memcpy(dst + i * destStride, src + i * srcStride, srcStride);
}
//解锁
ANativeWindow_unlockAndPost(nativeWindow);
av_frame_unref(frame);
}
}
}
在上面的代码中,因为转换成rgb格式过后的内容是存在ffmpeg所指向的地址而不是ANativeWindow所指向的所在地址,所以要绘制的话我们需要将内容复制到ANativeWindow中。
9、收尾释放资源
完成过后得释放资源,不然就造成内存泄露。
//释放
ANativeWindow_release(nativeWindow);
av_frame_free(&frame);
av_frame_free(&rgb_frame);
avcodec_close(mAvContext);
avformat_free_context(avFormatContext);
以上就实现了使用FFmpeg的解码新接口在JNI中对输入的视频进行解封装,解码,转成rgb并绘制到对应的显示款内,从而实现了视频播放。
10、java层界面绘制渲染相关
至于Java层,与上一篇旧接口一致,主要是创建一个SurfaceView用于视频播放使用,并传入Surface和视频路径,调用JNI接口,使用ffmpeg进行播放,具体就不阐述。
private SurfaceHolder mSurfaceHolder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
checkPermission();
mSurfaceview = (SurfaceView) findViewById(R.id.surfaceview);
mBtnPlay = (Button) findViewById(R.id.btnPlayVideo);
mBtnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
File file = new File(Environment.getExternalStorageDirectory(), "input.mp4");
runOnUiThread(new Runnable() {
@Override
public void run() {
render(file.getAbsolutePath(),mSurfaceHolder.getSurface());
}
});
}
});
SurfaceHolder holder = mSurfaceview.getHolder();
holder.addCallback(this);
// setType必须设置,要不出错.
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
// 将holder,这个holder为开始在onCreate里面取得的holder,将它赋给mSurfaceHolder
mSurfaceHolder = surfaceHolder;
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mSurfaceview = null;
mSurfaceHolder = null;
}
三、demo运行
与上一篇一致,demo中指定了播放视频源文件是/sdcard/input.mp4,如下代码,若要更新播放视频文件,可以在此处修改:
mBtnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
File file = new File(Environment.getExternalStorageDirectory(), "input.mp4");
runOnUiThread(new Runnable() {
@Override
public void run() {
render(file.getAbsolutePath(),mSurfaceHolder.getSurface());
}
});
}
});
运行界面如下:
点击PLAY播放:
完整例子已经放到github上,如下
https://github.com/weekend-y/FFmpeg_Android_Demo/tree/master/mydemo3_videoPlay2