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

2023-05-16

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

主要流程是
解封装

文章目录

    • Java层的主要配置
      • 1 app module build.grdle配置
      • 2 cmake文件配置
      • 3 创建GLSurfaceView的子类
    • Native层的相关代码
      • 1 初始化
      • 2 视解码器
      • 3 音频解码器
      • 4 开始解码
      • 5 硬解码和多线层解码性能测试

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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android播放器(一) 通过FFmpeg解码为RGBA格式播放 的相关文章

随机推荐

  • 解决:使用 Vue 3 Script Setup 时 ESLint 报错 ‘defineProps‘ is not defined

    解决 xff1a 使用 Vue 3 Script Setup 时 ESLint 报错 defineProps is not defined Vue 3 的 Script Setup 语法引入了 defineProps defineEmits
  • Wek6 A - Tree diameter

    问题描述 xff1a 实验室里原先有一台电脑 编号为1 xff0c 最近氪金带师咕咕东又为实验室购置了N 1台电脑 xff0c 编号为2到N 每台电脑都用网线连接到一台先前安装的电脑上 但是咕咕东担心网速太慢 xff0c 他希望知道第i台电
  • CSCSWek12 B-Happy 消消乐

    题目描述 Q老师是个很老实的老师 xff0c 最近在积极准备考研 Q老师平时只喜欢用Linux系统 xff0c 所以Q老师的电脑上没什么娱乐的游戏 xff0c 所以Q老师平时除了玩Linux上的赛车游戏SuperTuxKart之外 xff0
  • ubuntu中各个文件夹的作用

    Ubuntu的根目录的文件夹各个含义 home xff1a 家目录 xff0c 所有普通用户都有一个以自己名字命名的文件夹存放在这个目录中 普通用户登录ubuntu默认进入的就是家目录中自己的文件夹 xff0c 可用pwd命令查看 xff0
  • 【CUDA】Ubuntu系统如何安装CUDA保姆级教程(2022年最新)

    本期目录 Linux安装CUDA Linux安装CUDA 输入以下命令 xff0c 查看 GPU 支持的最高 CUDA 版本 笔者这里显示的是 11 6 xff0c 这意味着 xff0c 安装的 CUDA 版本必须 lt 61 11 6 n
  • AVI视频格式分析-封装格式

    AVI视频封装格式分析 使用的工具RIFF块CHUNK块LIST块hdrl LISTavih CHUNKstrl LISTstrh CHUNKstrf CHUNK JUNK CHUNKmovi LISTidx1 CHUNK 使用的工具 el
  • 2014.10.10

    1 主要是制作了suse镜像 xff0c 但是还存在很多问题 xff0c 没有加上默认网关 xff0c 我很不开心 xff0c 根目录没有扩展 2 了解了下 boot from image 通过glance上传一个镜像 xff0c 然后通过
  • 2014.10.11

    我只想骂csdn xff01 截图直接粘过来居然不能直接显示出来 xff01 xff01 xff01 妈蛋 xff01 xff01 1 suse镜像制作完善 xff0c 根目录未扩展这是个大问题 xff0c 默认网关没加上 所谓的根目录扩展
  • 2014.10.12

    早晨8点就起了 xff0c 然后匆匆奔向wx xff0c 为了思念的人 xff0c 吃了个中午饭 xff0c 感觉还不错 xff0c 下午回来之后又去了wpj xff0c 胡扯一通 xff0c 而且发现现在家里人的注意力完全放在我的情感生活
  • vmware 下安装 red hat 9,dos 以及wmware tools

    1 安装vmware vmware 版本 7 11 282343 英文原版下载 xff1a http dl sh ctc 2 pchome net 03 lt VMware workstation full 7 1 1 282343 rar
  • 关于上财陈畅的俄罗斯方块的学习

    最近同学学习C xff0c 想做一个大练习 xff0c 于是选择了俄罗斯方块 xff0c 我 xff0c 计算机专业在校学生 xff0c 说实话理论还行 xff0c 实践动手能力很差 xff0c 同学让我先做 xff0c 然后给他讲讲怎样一
  • xrdp开源项目的代码分析

    最近我的博客将重新恢复更新 xff0c 从2012年3月份起 xff0c 我开始参与某公司的堡垒机项目的研发工作 xff0c 堡垒机又叫内控堡垒机 xff0c 运维审计系统 xff0c 相信不少人也听说过 xff0c 目前电信 xff0c
  • xrdp开源项目的代码分析-1

    首先要说明情况 xff0c 我分析的代码基于xrdp 2012 5 11日 xff0c 而不是最新的代码 xff0c 最新的代码稍有改动 xff0c 但是主体的思想没有变化 xrdp 2012 5 11日代码的下载地址 xff1a http
  • 穿山甲的投放小技巧(账户如何快速过冷启动期)

    1 300 xff08 出价 xff1a 目标成本的2 3倍出价 xff09 xff0c 看成本 2 600 xff08 出价 xff1a 300预算时的一半 xff09 xff0c 看成本 3 放到日满格预算 xff08 出价 xff1a
  • C++加入库dll

    加入头文件加入 include 34 MES inc MES2Interface h 34 pragma comment lib 34 MES lib MES2Interface lib 34 MES2Interface dll 复制到运行
  • 结构体的大小如何计算

    我们实际生活中 xff0c 保存的数据一般不会是同一种类型 xff0c 所以引入了结构体 而结构体的大小也不是成员类型大小的简单相加 需要考虑到系统在存储结构体变量时的地址对齐问题 由于存储变量地址对齐的问题 xff0c 结构体大小计算必须
  • flatpak安装的firefox视频播放卡顿的解决方案

    最近在debian系统中使用flatpak安装最新版的firefox后发现 xff0c firefox在播放视频时十分卡顿 xff0c 经过四处搜索 xff0c 终于找到了解决方案 How to use hardware accelerat
  • NodeBB 安装部署 Linux(阿里云 CentOS 6.3 Redis NodeJS)

    网上有很多 xff0c 写的都不完整 xff0c 我尽量给大家一个完整的 基于Linux 阿里云 CentOS 6 3 安装 NodeBB 论坛 1 先安装NodeJs 安装方式有多种 xff0c 有通过下载源代码编译的 xff0c 有下载
  • shell脚本使用字符串截取报Bad substitution错误的原因即解决方法

    shell脚本使用字符串截取报Bad substitution错误的原因即解决方法 绝大多是是因为解释器的问题 第一步 使用命令查看你指令那个解释器 span class token function ls span bin sh al 我
  • Android播放器(一) 通过FFmpeg解码为RGBA格式播放

    代码可以参考 xff1a Github地址 本文主要介绍如何通过FFmpeg将MP4格式的视频数据解码为一帧一帧的RGBA像素格式数据来播放 因为主要是视频的解码及播放 xff0c 对于音频只是解码出了音频对应的pcm数据 xff0c 并没