美文网首页Android视频播放器
视频播放器之解封装

视频播放器之解封装

作者: arvinljw | 来源:发表于2019-01-31 17:14 被阅读94次

    在之前的文章中FFmpeg的编译集成也完成了,这一篇开始视频播放器处理的第一步:解封装

    解封装

    在解封装的代码开始以前,我们需要引入Log机制,虽然现在ndk开发中也能debug了但是有log会更方便。具体怎么引入在我的Android JNI开发系列之Java与C相互调用一文最后有方法,这里就不再赘述了。

    解封装这一步是处理视频数据的开始,需要处理以下几步:

    解封装.jpg

    Android的界面比较简单这里就不写了,样子是这样的:

    ui.png

    就两个按钮,第一个按钮把初始化和打开数据集成在一步了。

    准备工作

    为了方便测试,之后都把测试的方法定义在FFmpegUtil.java文件中,例如这里有初始化,打开数据文件和读取数据文件三个方法,写出来就是:

    public class FFmpegUtil {
        static {
            System.loadLibrary("native-lib");
        }
    
        public static native void init();
    
        public static native void open(String url);
    
        public static native void read();
    }
    

    然后实现都在native-lib.cpp文件中

    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
        //TODO 
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
        const char *url = env->GetStringUTFChars(url_, 0);
    
        //TODO 
    
        env->ReleaseStringUTFChars(url_, url);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
        //TODO 
    }
    

    当然实现还没有写。为了把每块的代码分开,所以我们把解封装的代码放到一个叫做Demux的cpp文件中,新建c++class Demux,然后在CMakeLists文件中添加到库中(不然找不到文件)。

    然后在Demux.h文件中定义三个方法:

    class Demux {
    public:
        virtual void init();
    
        virtual void open(const char *url);
    
        virtual bool read(); 
    }
    

    准备工作到这里就基本结束了,这三个方法的实现肯定都在Demux.cpp文件中。

    初始化

    其实没啥说的,就是调用FFmpeg的api,首先需要注册各种封装器和初始化网络;当然对于网络模块的初始化,是对在线视频才需要的。

    void Demux::init() {
        //注册所有封装器
        av_register_all();
        //初始化网络
        avformat_network_init();
        LOG_I("Register FFmpeg!");
    }
    

    这样写了,肯定会提示找不到方法,所以需要引入头文件,记住ffmpeg的库引入都需要加入extern "C",当然还有Log文件,如下:

    #include "Log.h"
    
    extern "C" {
    #include <libavformat/avformat.h>
    }
    

    这样初始化就完成了。

    打开数据文件

    核心方法就是avformat_open_input,需要传入AVFormatContext,这个上下文对象和文件的url以及其他配置信息,返回值是int,0表示成功,非0可以通过av_strerror转成对应的str信息提示。

    这一步就能把上下文对象初始化,然后再调用avformat_find_stream_info方法就能把常见的文件信息都获取到,参数就是传入上下文和配置字典(可不传)。

    然后带回读数据要区分是音频还是视频,所以可以通过av_find_best_stream方法获取到音频流的索引和视频流的索引。

    打开数据文件基本就这三个重要的方法,因为获取音频和视频信息的时候也能获取到对应的音视频参数,所以再在Demux.h中定义了两个方法,获取音频和视频的参数:

    virtual void getVideoParams();
    virtual void getAudioParams();
    

    当然里边我们用到的上下文和音视频流索引也在Demux.h中定义好:

    protected:
        AVFormatContext *ic;
        int videoStream = 0;
        int audioStream = 1;
    

    其中AVFormatContext肯定是找不到的,这时候也不要引用FFmpeg的头文件,避免耦合,可以定义成struct AVFormatContext;

    然后实现的方法如下:

    void Demux::open(const char *url) {
        LOG_I("open file %s begin", url);
        //打开文件
        int re = avformat_open_input(&ic, url, 0, 0);
        if (re != 0) {
            char buff[1024] = {0};
            av_strerror(re, buff, sizeof(buff));
            LOG_E("Demux open %s failed! error is %s", url, buff);
            return;
        }
        LOG_I("Demux open %s success", url);
    
        //读取文件信息
        re = avformat_find_stream_info(ic, 0);
        if (re != 0) {
            char buff[1024] = {0};
            av_strerror(re, buff, sizeof(buff));
            LOG_E("avformat_find_stream_info failed! error is %s", buff);
            return;
        }
        //读取总时长
        int64_t totalMs = ic->duration / (AV_TIME_BASE / 1000);
        LOG_I("total ms = %lld", totalMs);
    
        getVideoParams();
        getAudioParams();
    }
    
    void Demux::getVideoParams() {
        if (!ic) {
            return;
        }
        int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0, 0);
        if (re < 0) {
            LOG_E("av_find_best_stream video failed");
            return;
        }
        videoStream = re;
    }
    
    void Demux::getAudioParams() {
        if (!ic) {
            return;
        }
        int re = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, 0, 0);
        if (re < 0) {
            LOG_E("av_find_best_stream audio failed");
            return;
        }
        audioStream = re;
    }
    

    代码很简单,可以看到我们就能获取到总时长了和音视频的索引了。

    读取数据

    这一步是解封装这一步最核心的,因为通过这一步才获取到每一帧的数据;核心方法是av_read_frame,需要传入上下文ic,和AVPacket指针;而AVPacket指针的空间需要手动申请和释放,不然很容易造成内存泄露,所以这一点一定要注意,自己申请的数据一定要清理。

    还有一点就是packet返回帧信息中的pts和dps是有一个基数的,我们把它转成毫秒就好了,方便之后使用,在转换的过程中会涉及到一个AVRational类,是一个分数,但是包含分子和分母的,这样数据就更准确,一般这个基数是1000000。当然我们使用packet中提供的基数更准确,需要一个将AVRational转成double的方法:

    //分数转为浮点数
    static double r2d(AVRational r) {
        return r.num == 0 || r.den == 0 ? 0 : (double) r.num / (double) r.den;
    }
    

    很简单,就是判断分母不能为0,open方法的实现方式如下:

    bool Demux::read() {
        if (!ic) {
            return false;
        }
        AVPacket *pkt = av_packet_alloc();
        int re = av_read_frame(ic, pkt);
        if (re != 0) {
            av_packet_free(&pkt);
            return false;
        }
        pkt->pts = (long long) (pkt->pts * (1000 * r2d(ic->streams[pkt->stream_index]->time_base)));
        if (pkt->stream_index == audioStream) {
            LOG_I("read audio size = %d,pts = %lld", pkt->size, pkt->pts);
        } else if (pkt->stream_index == videoStream) {
            LOG_I("read video size = %d,pts = %lld", pkt->size, pkt->pts);
        } else {
            av_packet_free(&pkt);
            return false;
        }
        av_packet_free(&pkt);
        return true;
    }
    

    其中获取帧数据成功之后,转化pts和dps的时间基数,单位编程毫秒,然后再区分音频和视频去打印帧数据的大小和pts。当然其中av_packet_free是对AVPacket对象申请空间的释放。

    这样这三个方法的实现就基本完成了,然后我们再回到最开始,把native-lib中的方法实现一下,其实就是调用demux的方法。最后再在MainActivity中在点击不同按钮调用FFmpegUtil中的方法即可。native-lib.cpp代码如下:

    static Demux *demux;
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
        if (!demux) {
            demux = new Demux();
            demux->init();
        }
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
        const char *url = env->GetStringUTFChars(url_, 0);
    
        if (demux) {
            demux->open(url);
        }
    
        env->ReleaseStringUTFChars(url_, url);
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
        if (!demux) {
            return;
        }
        bool re = true;
        while (re) {
            re = demux->read();
        }
    }
    

    MainActivity中的代码如下:

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_init:
                initAndOpen();
                break;
            case R.id.btn_read_data:
                readData();
                break;
        }
    }
    private void initAndOpen() {
        permissionUtil.request("需要读取读写文件权限", Manifest.permission.WRITE_EXTERNAL_STORAGE,
                new PermissionUtil.RequestPermissionListener() {
                    @Override
                    public void callback(boolean granted, boolean isAlwaysDenied) {
                        FFmpegUtil.init();
                        FFmpegUtil.open("/sdcard/1080.mp4");
                    }
                });
    }
    private void readData() {
        FFmpegUtil.read();
    }
    

    这里的permissionUtil是我封装的对Android6.0以上动态申请权限库,方便使用。

    使用方法

    这里要打开数据文件所以需要文件读写权限,所以在AndroidManifest文件中也要申请权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    

    如果是在线视频文件需要再添加网络权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    

    当然在初始化部分网络初始化就一定要加上。

    上边的代码比较简单,就是FFmpegUtil.open的时候传入的url是自己本地的文件或者在线的视频才行。

    到这里解封装的基本内容就完了,还是比较简单的,当然如果有不正确的地方请不吝赐教。

    相关文章

      网友评论

        本文标题:视频播放器之解封装

        本文链接:https://www.haomeiwen.com/subject/dchlsqtx.html