FFmpeg_Android纯视频播放demo2--基于新接口

        在上一篇中,我们讲解了在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

猜你喜欢

转载自blog.csdn.net/weekend_y45/article/details/125161659