视频播放器--android端视频播放

一、准备阶段

开始之前,需要将FFmpeg集成到AS中,集成步骤可以参考之前的一一篇文章,链接地址linux如何编译ffmpeg,并集成到AS中。另外,需要额外依赖三个动态库libz.so(使用ffmpeg需要引入,这个软件包提供了用gzip和PKZIP压缩算法进行开发),libandroid.so(提供了视频渲染的窗口类ANativeWindow)和libOpenSLES.so(音频播放相关)。

二、大致流程

1、初始化FFmpeg(子线程中进行)

2、FFmpeg初始化完成以后,通知Activity可以播放了(采用native层反射java层通知)。activity调用native方法开始播放

3、首先开启packet队列和frame队列,然后开始视频播放(子线程),分为解码线程和播放线程

4、首先开启packet队列和frame队列,然后开始音频播放(子线程),分为解码线程和播放线程

5、音视频同步

6、资源释放

为什么要开启队列?一般情况下我们视频解码的速度是要比播放的速度快,这里的解码相当于生产者,播放相当于消费者。如果存放在数组,需要指定数组的大小,但是这个大小不好确定,太小的话这样很容易导致内存。所以使用队列的方式。

三、初始化FFmpeg

1、首先在播放之前需要将surfaceView进行初始化,当surfaceCreated()回调方法执行以后,表示已经准备好了。此时将Surface(可以通过surfaceHolder.getSurface()拿到)传递给native层,因为native层是可以对surface进行操作的。代码如下:

    // TODO java层
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        /** 
         * 将surface传递个native层
         */
        native_set_surface(mSurfaceHolder.getSurface());
    }


    // TODO native层
    Java_com_xinyartech_ffmpegdemo_player_QbPlayer2_native_1set_1surface(JNIEnv *env,         
         jobject instance,jobject surface) {

    
    //根据surface创建window 在之前需要释放掉window,防止横竖屏切换
    if(window){
        ANativeWindow_release(window);
        window = 0;
    }
    //初始化ANativeWindow
    window = ANativeWindow_fromSurface(env,surface);

}

2、点击播放按钮,会调用native层FFmpeg初始化方法。代码如下:

    /** TODO JAVA层
     * 
     * 调用native层初始化
     * @paramters dataSource 播放地址
     */
    public void prepare() {
        native_prepare(dataSource);
    }


    // TODO native层

    Java_com_xinyartech_ffmpegdemo_player_QbPlayer2_native_1prepare(JNIEnv *env, jobject             
           instance,jstring dataSource_) {
        const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
        JavaCallHelper* javaCallHelper = new JavaCallHelper(jvm,env,instance);
        // TODO  ffmpeg初始化
        fFmpeg = new QbFFmpeg(javaCallHelper,dataSource);
        //回调frame
        fFmpeg->setRenderFrameCallBack(renderFrame);
        fFmpeg->prepare();
        env->ReleaseStringUTFChars(dataSource_, dataSource);
    }

3、QbFFmpeg是一个封装实体类,主要负责初始化,解码操作。首先调用prepare()方法,代码如下:

/**
 * 在线程中进行ffmpeg初始化
 */
void QbFFmpeg::prepare() {
    pthread_create(&prepareThread, NULL, prepareFFmpeg, this);
}

4、开启子线程prepareFFmepg初始化,代码如下:

/**
 * 线程执行回调方法 一定要返回0
 */
void *prepareFFmpeg(void *args) {
    //强转对象,拿到当前外部操作对象
    QbFFmpeg *fFmpeg = static_cast<QbFFmpeg *>(args);
    fFmpeg->prepareFFmpegInit();
    return 0;

}

5、继续调用prepareFFmpegInit方法,代码如下:

/**
 * 这部分初始化是在子线程中
 */
void QbFFmpeg::prepareFFmpegInit() {

    //网络初始化
    avformat_network_init();
    //代表一个音频、视频。包含音频、视频的所有信息
    avFormatContext = avformat_alloc_context();

    //1、打开URL
    AVDictionary *options = NULL;
    //设置3秒链接超时
    av_dict_set(&options, "timeout", "3000000", 0);


    //为0返回成功
    int ret = avformat_open_input(&avFormatContext, url, NULL, &options);
    if (ret != 0) {
        LOGE("打开URL地址失败%s", url);

        //反射到java层,告诉UI,打开失败
        if (javaCallHelper) {
            javaCallHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
        }
        return;

    }
    //2、查找流
    if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
        if (javaCallHelper) {
            javaCallHelper->onError(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
        }
        return;
    }


    //3、根据流,找到解码器
    for (int i = 0; i < avFormatContext->nb_streams; ++i) {
        AVCodecParameters *parameters = avFormatContext->streams[i]->codecpar;
        AVCodec *avCodec = avcodec_find_decoder(parameters->codec_id);
        if (!avCodec) {
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
            }
            break;
        }
        //创建解码器上下文
        AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
        if (!avCodecContext) {
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
            }
        }
        //复制解码器参数
        if (avcodec_parameters_to_context(avCodecContext, parameters) < 0) {
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
            }
            return;
        }
        //打开解码器
        if (avcodec_open2(avCodecContext, avCodec, 0) != 0) {
            if (javaCallHelper) {
                javaCallHelper->onError(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
            }
            return;
        }

        //判断是音频还是视频
        if (parameters->codec_type == AVMEDIA_TYPE_VIDEO) {

            //视频
            videoChannel2 = new VideoChannel2(i, javaCallHelper, avCodecContext);
            //设置回调
            videoChannel2->setRenderFrameCallBack(renderFrame);

        } else if (parameters->codec_type == AVMEDIA_TYPE_AUDIO) {

            //音频
            audioChannel = new AudioChannel(i, javaCallHelper, avCodecContext);

        }
    }

    if (!videoChannel2 && !audioChannel) {
        if (javaCallHelper) {
            javaCallHelper->onError(THREAD_CHILD, FFMPEG_NOMEDIA);

        }
        return;
    }

    //表示ffmpeg初始化准备好了
    if (javaCallHelper) {
        javaCallHelper->onPrepare(THREAD_CHILD);
    }


}

这里对视频和音频封装类都进行了初始化,后面会用到。

二、反射JAVA层

1、看上面最后javaCallHelper->onPrepare(THREAD_CHILD)代码,表示已经初始化完成了,这里的javaCallHelper是一个反射封装类,会对视频初始化失败、成功以及播放进度反射给java层。代码如下:

#include "JavaCallHelper.h"
#include "macro.h"

JavaCallHelper::JavaCallHelper(JavaVM *javaVM, JNIEnv *env, jobject &obj) {
    this->javaVM = javaVM;
    this->env = env;
    //设置成全局的obj
    this->obj = env->NewGlobalRef(obj) ;

    jclass jClazz = env->GetObjectClass(obj);

    //对应java层的三个方法
    jmid_prepare = env->GetMethodID(jClazz,"onPrepare","()V");
    jmid_progress = env->GetMethodID(jClazz,"onProgress","(I)V");
    jmid_error = env->GetMethodID(jClazz,"onError","(I)V");
}

JavaCallHelper::~JavaCallHelper() {

}

/**
 * 以下方法反射java方法
 * @param thread
 * @param errorCode
 */
void JavaCallHelper::onError(int thread, int errorCode) {
    if(thread == THREAD_CHILD){
        JNIEnv* bindEnv = NULL;
        //绑定到主线程
        if(javaVM->AttachCurrentThread(&bindEnv,0) != JNI_OK){
            return;
        }
        //使用绑定之后的env结构体
        bindEnv->CallVoidMethod(obj,jmid_error,errorCode);
        //解绑
        javaVM->DetachCurrentThread();
    }else{
        //主线程直接反射
        env->CallVoidMethod(obj,jmid_error,errorCode);
    }
}

void JavaCallHelper::onProgress(int thread, int progress) {
    if(thread == THREAD_CHILD){
        JNIEnv* bindEnv = NULL;
        //绑定到主线程
        if(javaVM->AttachCurrentThread(&bindEnv,0) != JNI_OK){
            return;
        }
        //使用绑定之后的env结构体
        bindEnv->CallVoidMethod(obj,jmid_progress,progress);
        //解绑
        javaVM->DetachCurrentThread();
    }else{
        //主线程直接反射
        env->CallVoidMethod(obj,jmid_progress,progress);
    }
}

void JavaCallHelper::onPrepare(int thread) {
    if(thread == THREAD_CHILD){
        JNIEnv* bindEnv = NULL;
        //绑定到主线程
        if(javaVM->AttachCurrentThread(&bindEnv,0) != JNI_OK){
            return;
        }
        //使用绑定之后的env结构体
        bindEnv->CallVoidMethod(obj,jmid_prepare);
        //解绑
        javaVM->DetachCurrentThread();
    }else{
        //主线程直接反射
        env->CallVoidMethod(obj,jmid_prepare);
    }
}

2、此时会回调java层onPrepare方法

     /**
     * 接受native层反射的方法 ,再通过接口回调给activity
     */

    //准备
    public void onPrepare() {
        if (mPrepareListener != null) {
            mPrepareListener.onPrepared();
        }

    }


    /** TODO activity接收处理代码
     * 初始化回调
     */
    private void initCallBack() {
        mQbPlayer.setPrepareListener(new QbPlayer2.prepareListener() {
            @Override
            public void onPrepared() {
                //ffmpeg初始化完成才能进行播放
                Log.e("初始化完成了11-----","11111111111");
                mQbPlayer.start();

            }
        });
        mQbPlayer.setProgressListener(new QbPlayer2.progressListener() {
            @Override
            public void onProgree(int progress) {

            }
        });
        mQbPlayer.setErrorListener(new QbPlayer2.errorListener() {
            @Override
            public void onError(int errorCode) {

            }
        });

    }

3、start方法调用以后会调用native层,代码如下:

Java_com_xinyartech_ffmpegdemo_player_QbPlayer2_native_1start(JNIEnv *env, jobject instance) {

    // TODO ffmpeg配置完成后 开始播放
    if(fFmpeg){
        fFmpeg->start();
    }

}

三、视频解码

1、首先在fFpeng类中会开启一个不断读取packet的子线程,并且把相应packet类型(视频类型,音频类型)存放到不同的音视频frame队列中。

/**
 * 解码线程
 */
void *play(void *args) {
    QbFFmpeg *fFmpeg = static_cast<QbFFmpeg *>(args);
    fFmpeg->startDecode();
    return 0;
}

void QbFFmpeg::start() {
    isPlaying = true;
    //视频开始播放 交给视频对象处理
    if (videoChannel2) {
        videoChannel2->play();
    }

    //开始解码线程
    pthread_create(&playThread, NULL, play, this);
}





void QbFFmpeg::startDecode() {
    int ret = 0;
    while (isPlaying) {

        //防止队列满了,生产者速度大于消费者速度 100帧 放在前面,此时没有去取一包,防止跳帧
        if (audioChannel && audioChannel->pkt_queue.size() > 100) {
            av_usleep(10 * 1000);//休眠10ms
            continue;
        }

        if (videoChannel2 && videoChannel2->pkt_queue.size() > 100) {
            av_usleep(10 * 1000);
            continue;
        }

        //读取包

        /** TODO AVPacket 结构体 相关信息 解码前数据
         *  uint8_t *data:压缩编码的数据。
            例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
            注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
            因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
            int   size:data的大小
            int64_t pts:显示时间戳
            int64_t dts:解码时间戳
            int   stream_index:标识该AVPacket所属的视频/音频流。
         */
        AVPacket *packet = av_packet_alloc();
        //从媒体中读取音频、视频包
        ret = av_read_frame(avFormatContext, packet);

        if (ret == 0) {
            //将数据包加入队列
            if (audioChannel && packet->stream_index == audioChannel->channelId) {
                // audioChannel->pkt_queue.enQueue(packet);
            } else if (videoChannel2 && packet->stream_index == videoChannel2->channelId) {
                videoChannel2->pkt_queue.enQueue(packet);
            }

        } else if (ret == AVERROR_EOF) {
            //读取完毕了,但不一定是播放完毕
            if (videoChannel2->pkt_queue.empty() && videoChannel2->frame_queue.empty() &&
                audioChannel->pkt_queue.empty() && audioChannel->frame_queue.empty()) {
                break;
            }
        } else {
            break;
        }

    }
    //停止
    isPlaying = false;
    videoChannel2->isPlaying = false;
    audioChannel->stop();
    videoChannel2->stop();

}

2、在启动读取packet的过程中,同时调用了videoChannel2->play();方法。代码如下:

#include "VideoChannel2.h"
#include "BaseChannel.h"

extern "C" {
#include <libavutil/time.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
}

/**
 * 解码线程执行方法
 */
void *decode(void *args) {
    VideoChannel2 *videoChannel2 = static_cast<VideoChannel2 *>(args);
    videoChannel2->decodePackets();
    return 0;
}

/**
 * 播放线程执行方法
 */
void *goPlay(void *args) {
    VideoChannel2 *videoChannel2 = static_cast<VideoChannel2 *>(args);
    videoChannel2->goPlayFrame();
    return 0;
}


VideoChannel2::VideoChannel2(int id, JavaCallHelper *javaCallHelper,
                             AVCodecContext *avCodecContext) : BaseChannel(id, javaCallHelper,
                                                                           avCodecContext) {
}

VideoChannel2::~VideoChannel2() {

}

void VideoChannel2::play() {
    //使队列开始工作
    pkt_queue.setWork(1);
    frame_queue.setWork(1);
    isPlaying = true;
    //解码线程
    pthread_create(&pid_video_decode, NULL, decode, this);
    //播放线程
    pthread_create(&pid_video_play, NULL, goPlay, this);


}

void VideoChannel2::stop() {

}

void VideoChannel2::decodePackets() {
    AVPacket *packet = 0;
    LOGE("解码一帧11");
    while (isPlaying) {
        //到QbFfmpeg类中进行解码,
        //懂队列中读取packet
        int ret = pkt_queue.deQueue(packet);
        if (!isPlaying) {
            break;
        }
        if (!ret) {
            continue;
        }
        //开始解压
        ret = avcodec_send_packet(avCodecContext, packet);
        //得到frame,此时可以释放packet
        releaseAvPacket(packet);
        //解码器中也维持一个解码队列,满的话就需要等待
        if (ret == AVERROR(EAGAIN)) {
            //需要更多数据
            continue;
        } else if (ret < 0) {
            //失败 比如断线了
            break;
        }
        //读取frame
        AVFrame *frame = av_frame_alloc();
        ret = avcodec_receive_frame(avCodecContext, frame);

        LOGE("加入一帧");
        frame_queue.enQueue(frame);
        //同样判断队列是否需要延迟,防止队列爆满
        while (frame_queue.size() > 100 && isPlaying) {
            av_usleep(1000 * 10);
            continue;
        }


    }
    //彻底释放完毕 释放最后一帧
    releaseAvPacket(packet);
}
void VideoChannel2::goPlayFrame() {
    //初始化转换器上下文 YUV转rgba
    SwsContext *swsContext = sws_getContext(avCodecContext->width, avCodecContext->height,
                                            avCodecContext->pix_fmt,
                                            avCodecContext->width, avCodecContext->height,
                                            AV_PIX_FMT_RGBA, SWS_BILINEAR, 0, 0, 0);

    uint8_t *dst_data[4];//4个字节argb
    int dst_lineSize[4];
    //初始化容器,刚好和一帧的大小相等
    av_image_alloc(dst_data, dst_lineSize, avCodecContext->width, avCodecContext->height,
                   AV_PIX_FMT_RGBA, 1);
    AVFrame *frame = 0;
    while (isPlaying) {
        int ret = frame_queue.deQueue(frame);
        //播放停止了,退出循环
        if (!isPlaying) {
            break;
        }
        //没有取到,继续取
        if (!ret) {
            continue;
        }



        //填充容器 frame->data指向一行的首地址 数据转换到dst_data
        /**
         * 第二个参数可以理解为1帧为N个片(片头+片数据),是数组
         * 第三个参数是修饰每个片数据长度,一行的像素数据
         * 第四个参数,一帧的高度
         *
         * 第五个参数 像素总个数 指向首地址
         * 第6个参数 每一行的像素个数
         */
        sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
                  frame->linesize, 0, frame->height, dst_data, dst_lineSize);

        //回调给调用者
        renderFrame(dst_data[0],dst_lineSize[0],avCodecContext->width,avCodecContext->height);
        //16ms
        av_usleep(16*1000);
        //释放frame
        releaseAvFrame(frame);
    }

    //资源释放
    av_free(&dst_data[0]);
    isPlaying = false;
    releaseAvFrame(frame);
    sws_freeContext(swsContext);
}

void VideoChannel2::setRenderFrameCallBack(RenderFrame renderFrame) {
        this->renderFrame = renderFrame;

}

3、在play执行以后会启动两个线程:解码线程(将packet转为frame)和播放线程。在转为frame过程中为了防止frame队列爆满,当队列长度超过100时就会延迟一段时间,等待frame被消费。消费frame是调用renderFrame(dst_data[0],dst_lineSize[0],avCodecContext->width,avCodecContext->height);回调给native_lib.cpp类。因为ANativeWindow是仅存在于这个类的,播放的渲染需要在这里执行。回调渲染代码如下:

void renderFrame(uint8_t* data,int lineSize,int w,int h){
    //    渲染
    //设置窗口属性
    ANativeWindow_setBuffersGeometry(window, w,
                                     h,
                                     WINDOW_FORMAT_RGBA_8888);

    ANativeWindow_Buffer window_buffer;
    if (ANativeWindow_lock(window, &window_buffer, 0)) {
        ANativeWindow_release(window);
        window = 0;
        return;
    }
//    缓冲区  window_data[0] =255;
    uint8_t *window_data = static_cast<uint8_t *>(window_buffer.bits);
//    r     g   b  a int 一行的像素 字节拷贝 需要x4
    int window_linesize = window_buffer.stride * 4;
    //数据源
    uint8_t *src_data = data;

    //一个字节拷贝速度太慢
    //一行一行的拷贝
    //不能整体拷贝原因:缓冲区一行的大小可能会比实际的一行数据大,整体拷贝会导致是在缓存去一行会去填充实际的两行数据,导致视频画面乱码
    for (int i = 0; i < window_buffer.height; ++i) {
        //大小以左边为准,根据真实为准
        memcpy(window_data + i * window_linesize, src_data + i * lineSize, window_linesize);
    }
    ANativeWindow_unlockAndPost(window);





}
以上就完成了视频的渲染播放。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值