Android播放器(一) 通过FFmpeg解码为RGBA格式播放

代码可以参考: Github地址
本文主要介绍如何通过FFmpeg将MP4格式的视频数据解码为一帧一帧的RGBA像素格式数据来播放。
因为主要是视频的解码及播放,对于音频只是解码出了音频对应的pcm数据,并没有播放pcm。因此也不会涉及到音视频的同步。

主要流程是
解封装

Java层的主要配置

首先建一个支持cpp的项目

1 app module build.grdle配置

externalNativeBuild {
            cmake {
            	//cpp编译器flag
                cppFlags "-std=c++11"
            }

            ndk{
                //指定所支持的cpu架构
                abiFilters "armeabi-v7a"
            }
        }

        sourceSets{
            main{
                //指定ffmpeg路径
                jniLibs.srcDirs=['libs']
            }
        }

指定cmake路径

android {
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

2 cmake文件配置

#1 声明cmake版本
cmake_minimum_required(VERSION 3.4.1)


#2 添加头文件路径(相对于本文件路径)
include_directories(include)

#3 设置ffmpeg库所在路径的变量
set(FF ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI})

# 4添加ffmpeg相关库

# 4.1解码
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FF}/libavcodec.so)

# 4.2格式转换
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${FF}/libavformat.so)

# 4.3基础库
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${FF}/libavutil.so)

# 4.4格式转换
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${FF}/libswscale.so)

#4.5 音频重采样
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${FF}/libswresample.so)

#5 指定本地cpp文件 和 打包对应的so库
add_library(native-lib
            SHARED
            src/main/cpp/native-lib.cpp
            )

find_library(log-lib
              log )
# 7 链接库
target_link_libraries(native-lib
                      avcodec avformat avutil swscale swresample 
                      android
                       ${log-lib} )

3 创建GLSurfaceView的子类

这一步骤主要是在开启的子线程中,将GLSurfaceView的Surface传递到底层来渲染数据

//1 创建GlSurfaceView的子类
public class PlayView  extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {

    public PlayView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

   public void start(){
    	//2 手动创建线程
        new Thread(this).start();
    }
    
    @Override
    public void run() {
        String videoPath = Environment.getExternalStorageDirectory() + "/video.mp4";
        //3 在子线程中 将视频url和Surface对象传递到native层
        open(videoPath, getHolder().getSurface());
    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    	//4 android8.0必须调用此方法,否则无法显示
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            setRenderer(this);
        }
    }
    public native void open(String url, Object surface);

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {

    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int i, int i1) {

    }

    @Override
    public void onDrawFrame(GL10 gl10) {

    }
}

Native层的相关代码

1 初始化

首先注册解封装器,打开文件。然后就可以拿到封装数据里面的音频和视频的索引。

 //1 初始化解封装
    av_register_all();

    AVFormatContext *ic = NULL;

    //2 打开文件
    int re = avformat_open_input(&ic, path, 0, 0);

    if (re != 0) {
        LOGEW("avformat_open_input %s success!", path);
    } else {
        LOGEW("avformat_open_input failed!: %s", av_err2str(re));
    }

    //3 获取流信息
    re = avformat_find_stream_info(ic, 0);
    if (re != 0) {
        LOGEW("avformat_find_stream_info failed!");
    }
    LOGEW("duration = %lld nb_streams = %d", ic->duration, ic->nb_streams);

    int fps = 0;
    int videoStream = 0;
    int audioStream = 1;

    //4 获取视频音频流位置
    for (int i = 0; i < ic->nb_streams; i++) {
        AVStream *as = ic->streams[i];
        if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            LOGEW("视频数据");
            videoStream = i;
            fps = r2d(as->avg_frame_rate);

            LOGEW("fps = %d, width = %d height = %d codeid = %d pixformat = %d", fps,
                  as->codecpar->width,
                  as->codecpar->height,
                  as->codecpar->codec_id,
                  as->codecpar->format);

        } else if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            LOGEW("音频数据");
            audioStream = i;
            LOGEW("sample_rate = %d channels = %d sample_format = %d",
                  as->codecpar->sample_rate,
                  as->codecpar->channels,
                  as->codecpar->format);
        }
    }

对于多路流可以通过遍历的方式拿到音频和视频流的索引地址,也可以直接指定流数据类型来获取索引地址

//5 获取音频流信息 和上面遍历取出视音频的流信息是一样的,这种方式更直接
audioStream = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);

2 视解码器

在这一步骤中,首先定义并初始化解码器,然后视频流索引的地址传给解码器。最后打开解码器。

	//1 软解码器
    AVCodec *vcodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);

    if (!vcodec) {
        LOGEW("avcodec_find failed");
    }

    //2 解码器初始化
    AVCodecContext *vc = avcodec_alloc_context3(vcodec);

    //3 解码器参数赋值
    avcodec_parameters_to_context(vc, ic->streams[videoStream]->codecpar);

   // 定义解码的线程
    vc->thread_count = 8;

    //4 打开解码器
    re = avcodec_open2(vc, 0, 0);
    LOGEW("vc timebase = %d/ %d", vc->time_base.num, vc->time_base.den);

    if (re != 0) {
        LOGEW("avcodec_open2 video failed!");
    }

需要注意的是,这里可以自定义解码的线程,来控制解码速度。可以自定义大小,后续代码会有介绍调整这个值以后,来测试解码速度大小。

另外,这部分只是通过软解码的方式,软解比较消耗CPU,但是兼容性好
硬解不消耗CPU,更省电,但是硬解可能会有兼容性的问题

下面是获取硬解码器

AVCodec *vcodec = avcodec_find_decoder_by_name("h264_mediacodec");

在使用硬解码器,还要额外定义此方法。用于确保在获取硬解码器之前调用av_jni_set_java_vm()函数,通过调用av_jni_set_java_vm()才可以获取到硬解码器。

extern "C"
JNIEXPORT
jint JNI_OnLoad(JavaVM *vm,void *res)
{
    av_jni_set_java_vm(vm,0);
    return JNI_VERSION_1_4;
}

3 音频解码器

和之前的视频解码器的步骤相同,只是部分参数不同

	//1 软解码器
	AVCodec *acodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id);

    if (!acodec) {
        LOGEW("avcodec_find failed!");
    }

    //2 解码器初始化
    AVCodecContext *ac = avcodec_alloc_context3(acodec);
    avcodec_parameters_to_context(ac, ic->streams[audioStream]->codecpar);
    ac->thread_count = 1;

    //3 打开解码器
    re = avcodec_open2(ac, 0, 0);
    if (re != 0) {
        LOGEW("avcodec_open2 audio failed!");
    }    

4 开始解码

以下是解码过程的完整代码

 	//1 定义Packet和Frame
    AVPacket *pkt = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();

    //用于测试性能
    long long start = GetNowMs();
    int frameCount = 0;

    //2 像素格式转换的上下文
    SwsContext *vctx = NULL;

    int outwWidth = 1280;
    int outHeight = 720;
    char *rgb = new char[1920*1080*4];
    char *pcm = new char[48000*4*2];

    //3 音频重采样上下文初始化
    SwrContext *actx = swr_alloc();
    actx = swr_alloc_set_opts(actx,
                              av_get_default_channel_layout(2),
                              AV_SAMPLE_FMT_S16,
                              ac->sample_rate,
                              av_get_default_channel_layout(ac->channels),
                              ac->sample_fmt,ac->sample_rate,0,0);

    re = swr_init(actx);
    if(re != 0)
    {
        LOGEW("swr_init failed!");
    }else
    {
        LOGEW("swr_init success!");
    }

    //4 显示窗口初始化
    ANativeWindow *nwin = ANativeWindow_fromSurface(env,surface);
    ANativeWindow_setBuffersGeometry(nwin,outwWidth,outHeight,WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer wbuf;

    for (;;) {

        //这里是测试每秒解码的帧数  每三秒解码多少帧
        if(GetNowMs() - start >= 3000)
        {
            LOGEW("now decode fps is %d", frameCount/3);
            start = GetNowMs();
            frameCount = 0;
        }

        int re = av_read_frame(ic, pkt);
        if (re != 0) {
            LOGEW("读取到结尾处!");
            int pos = 20 * r2d(ic->streams[videoStream]->time_base);
            av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
            break;
        }

        AVCodecContext *cc = vc;
        if (pkt->stream_index == audioStream) {
            cc = ac;
        }

        //1 发送到线程中解码
        re = avcodec_send_packet(cc, pkt);

        //清理
        int p = pkt->pts;
        av_packet_unref(pkt);

        if (re != 0) {
            LOGEW("avcodec_send_packet failed!");
            continue;
        }

        //每一帧可能对应多个帧数据,所以要遍历取
        for (;;) {
            //2 解帧数据
            re = avcodec_receive_frame(cc, frame);
            if (re != 0) {
                break;
            }
            //LOGEW("avcodec_receive_frame %lld", frame->pts);

            //如果是视频帧
            if(cc == vc){
                frameCount++;

                //3 初始化像素格式转换的上下文
                vctx = sws_getCachedContext(vctx,
                                            frame->width,
                                            frame->height,
                                            (AVPixelFormat)frame->format,
                                            outwWidth,
                                            outHeight,
                                            AV_PIX_FMT_RGBA,
                                            SWS_FAST_BILINEAR,
                                            0,0,0);

                if(!vctx){
                    LOGEW("sws_getCachedContext failed!");
                }else
                {
                    uint8_t  *data[AV_NUM_DATA_POINTERS] = {0};
                    data[0] = (uint8_t *)rgb;
                    int lines[AV_NUM_DATA_POINTERS] = {0};
                    lines[0] = outwWidth * 4;
                    int h = sws_scale(vctx,
                                      (const uint8_t **)frame->data,
                                      frame->linesize,
                                      0,
                                      frame->height,
                                      data,
                                      lines);
                    LOGEW("sws_scale = %d",h);

                    if(h > 0)
                    {
                        ANativeWindow_lock(nwin,&wbuf,0);
                        uint8_t  *dst = (uint8_t*)wbuf.bits;
                        memcpy(dst, rgb, outwWidth*outHeight*4);
                        ANativeWindow_unlockAndPost(nwin);
                    }
                }

            }else //音频帧
            {
                uint8_t  *out[2] = {0};
                out[0] = (uint8_t*)pcm;

                //音频重采样
                int len = swr_convert(actx,out,frame->nb_samples,(const uint8_t**)frame->data,frame->nb_samples);
                LOGEW("swr_convert = %d", len);
            }
        }
    }

    delete rgb;
    delete pcm;

1 这段代码中,外层循环通过av_read_frame方法来给packet赋值,因为一个packet可能对应多个frame,所以packet每次通过avcodec_send_packet()方法发送到解码线程后,需要多次调用avcodec_receive_frame()来获取frame。
2 测试性能部分通过每三秒解码的帧数,除以3来计算平均每秒解码的帧数。
下面是获取当前时间的方法

long long GetNowMs()
{
    struct timeval tv;
    gettimeofday(&tv,NULL);
    int sec = tv.tv_sec%360000;
    long long t = sec*1000+tv.tv_usec/1000;
    return t;
}

3 对于获取播放时间戳pts
在ffmpeg中用的是分数的时间基AVRational来表示时间的基本单位。
AVRational有两个变量分子和分母

typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

例如时间基为1/1000。
通过转换可以把此分数转换为浮点数

static double r2d(AVRational rational)
{
    return rational.num == 0 || rational.den == 0 ? 0.:(double)rational.num/ (double)rational.den;
}

这样就能算出每一帧对应的时间戳pts

 pkt->pts = pkt->pts * (1000*r2d(ic->streams[pkt->stream_index]->time_base));

获取解码时间戳dts同理

5 硬解码和多线层解码性能测试

1 单线程解码平局速度

now decode fps is 18
now decode fps is 18
now decode fps is 19

2 六线程解码均速

now decode fps is 105

3 硬解码均速

now decode fps is 95
now decode fps is 92
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值