美文网首页FFmpegAndroid FFMPEGandroid经验总结
【Android 音视频开发打怪升级:FFmpeg音视频编解码篇

【Android 音视频开发打怪升级:FFmpeg音视频编解码篇

作者: 开发的猫 | 来源:发表于2020-02-11 10:19 被阅读0次

    【声 明】

    首先,这一系列文章均基于自己的理解和实践,可能有不对的地方,欢迎大家指正。
    其次,这是一个入门系列,涉及的知识也仅限于够用,深入的知识网上也有许许多多的博文供大家学习了。
    最后,写文章过程中,会借鉴参考其他人分享的文章,会在文章最后列出,感谢这些作者的分享。

    码字不易,转载请注明出处!

    教程代码:【Github传送门

    目录

    一、Android音视频硬解码篇:

    二、使用OpenGL渲染视频画面篇

    三、Android FFmpeg音视频解码篇

    • 1,FFmpeg so库编译
    • 2,Android 引入FFmpeg
    • 3,Android FFmpeg视频解码播放
    • 4,Android FFmpeg+OpenSL ES音频解码播放
    • 5,Android FFmpeg+OpenGL ES播放视频
    • 6,Android FFmpeg简单合成MP4:视屏解封与重新封装
    • 7,Android FFmpeg视频编码

    本文你可以了解到

    本文将介绍如何将上一篇文章编译出来的 FFmpeg so 库,引入到 Android 工程中,并验证 so 是否可以正常使用。

    一、开启 Android 原生 C/C++ 支持

    在过去,通常使用 makefile 的方式在项目中引入 C/C++ 代码支持,随着 Android Studio 的普及,makefile 的方式已经基本被 CMake 替代。

    有了 Android 官方的支持,NDK 层代码的开发变得更加容易。以前一谈到 Android NDK ,许多人就会大惊失色,感觉是深不可测的东西,一方面是 makefile 的编写很难,一方面是 C/C++ 相比 Java 来说,比较晦涩。

    但是不必担心,一是有了 CMake ,二是对于 C/C++ 的基本使用其实和 Java 差不多,本系列涉及到的,也都是对 C/C++ 的基础使用,毕竟,高级的我也不会不是吗?哈哈哈~~

    1. 安装 CMake

    首先,需要下载 CMake 相关工具,在 Android Studio 中依次点击 Tools->SDK Manager->SDK Tools,然后勾选

    CMake : CMake 构建工具

    LLDB : C/C++ 代码调试工具

    NDK : NDK 环境

    最后依次点击 OK->OK->Finish ,开始下载(文件比较大,可能会比较慢,请耐心等待)。

    下载CMake工具

    2. 添加 C/C++ 支持

    有两种方式:

    一是,新建一个新的工程,并勾选 C/C++ 支持选项,系统将自动创建一个支持 C/C++ 编码的工程。

    二是,在已有的项目上,手动添加所有的添加项来支持 C/C++ 编码,其实就是自己手动添加「第一种方式」Android Studio 为我们自动创建的那些东西。

    首先,通过新建一个新工程的方式,看看 IDE 为我们生成了那些东西。

    1)新建 C/C++ 工程

    依次点击 File -> New -> New Project,进入新建工程页面,拉到最后,选择 Native C++ 然后按照默认配置,一路 Next -> Next -> Finish 即可。

    新建C++工程
    2)Android Studio 自动生成了什么

    生成的工程目录如下:

    工程目录

    重点关注上图标注的3个地方:

    • 第一,最上层的 MainActivity
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // Example of a call to a native method
            sample_text.text = stringFromJNI()
        }
    
        /**
         * A native method that is implemented by the 'native-lib' native library,
         * which is packaged with this application.
         */
        external fun stringFromJNI(): String
    
        companion object {
    
            // Used to load the 'native-lib' library on application startup.
            init {
                System.loadLibrary("native-lib")
            }
        }
    }
    

    很简单,使用过 so 库的应该都看得懂,这里简单说一下。

    代码的最下面,companion objectKotlin 中表示静态代码块,类似 Java 中的 static { },其中的代码有且只会被执行一次。

    接着在 init{} 方法中,加载了 C/C++ 代码编译成的 so 库: native-lib

    往上一句代码,用 external 声明了一个外部引用的方法 stringFromJNI() ,这个方法和 C/C++ 层的代码是对应的。

    最终在最上面的 onCreate 中,将从 C/C++ 层返回的 String 显示出来。

    • 第二,创建了一个 cpp 文件包

    其中有两个文件非常重要,分别是 native-lib.cppCMakeLists.txt

    i. native-lib.cpp :是一个 C++ 接口文件,在 MainActivity 中声明的外部方法将在这里得到实现。

    自动生成 native-lib.cpp 的内容如下:

    #include <jni.h>
    #include <string>
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_chenlittleping_mynativeapp_MainActivity_stringFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    

    可以看到,这个 cpp 文件中的方法命名非常的长,不过其实非常简单。

    首先是头部固定写法 extern "C" JNIEXPORT jstring JNICALL

    extern "C" 表示以 C语言 的方式来编译;

    jstring 表示该方法返回类型是 Java 层的 String 类型,类似的还是有: void jint等;

    然后是 Java 层对应方法的映射,即整个方法命名其实是 Java 层对应方法的绝对路径。

    其中,最前面的 Java_ 是固定写法;

    com_chenlittleping_mynativeapp_MainActivity_: 对应的是 com.chenlittleping.mynativeapp.MainActivity.,其实就是 . 换为 _

    stringFromJNI 和 Java 层的方法一致。

    最后是两个参数JNIEnv *envjobject,分别代表 JNI 的上下文环境和调用这个接口的 Java 的类的实例。

    调用这个方法,将会在 C++ 层创建一个字符串,并以 Java#String 的类型返回。

    ii. CMakeLists.txt : 也就是构建脚本。内容如下:

    # cmake 最低版本
    cmake_minimum_required(VERSION 3.4.1)
    
    # 配置so库编译信息
    add_library( 
            # 输出so库的名称
            native-lib
    
            # 设置生成库的方式,默认为SHARE动态库
            SHARED
    
            # 列出参与编译的所有源文件
            native-lib.cpp)
    
    # 查找代码中使用到的系统库
    find_library( # Sets the name of the path variable.
            log-lib
    
            # Specifies the name of the NDK library that
            # you want CMake to locate.
            log)
    
    # 指定编译目标库时,cmake要链接的库
    target_link_libraries(
            # 指定目标库,native-lib 是在上面 add_library 中配置的目标库
            native-lib
    
            # 列出所有需要链接的库
            ${log-lib})
    

    这是最简单的编译配置,具体见上面的注释。

    CMakeLists.txt 的目的就是配置可以编译出 native-lib so 库的构建信息。

    说白了,就是告诉编译器:

    - 编译的目标是谁
    - 依赖的源文件在哪里找
    - 依赖的 `系统或第三方` 的 `动态或静态` 库在哪里找。
    
    • 第三,在 Gradle 文件中注册 CMake 脚本

    第二步 中,已经把构建 so 库的信息配置好了,接下来要把这些信息注册到 Gradle 中,编译器才会去编译它。

    app 的 build.gradle 内容如下:

    apply plugin: 'com.android.application'
    
    apply plugin: 'kotlin-android'
    
    apply plugin: 'kotlin-android-extensions'
    
    android {
        compileSdkVersion 28
        buildToolsVersion "29.0.1"
        defaultConfig {
            applicationId "com.chenlittleping.mynativeapp"
            minSdkVersion 19
            targetSdkVersion 28
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
            
            // 1) CMake 编译配置
            externalNativeBuild {
                cmake {
                    cppFlags ""
                }
            }
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        
        // 2) 配置 CMakeLists 路径
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt"
                version "3.10.2"
            }
        }
    }
    
    dependencies {
        // 省略无关代码
        //......
    }
    
    

    最主要的两个地方是两个 externalNativeBuild

    第 1 个 externalNativeBuild 中,可以做一些优化配置,比如只打包包含 armeabi 架构的 so

    externalNativeBuild {
        cmake {
            cppFlags ""
        }
        ndk {
            abiFilters  "armeabi" //, "armeabi-v7a"
        }
    }
    

    第 2 个 externalNativeBuild,主要是配置 CMakeLists.txt 的路径和版本。

    Android Studio 为我们生成的关于 C/C++ 支持的主要就是以上三个地方,有了以上配置,就可以在 MainActivity 页面中正常的显示出 Hello from C++

    3) 在已有工程上添加 C/C++ 支持

    前面就说过,在已有项目上添加 C/C++ 支持,就是由我们自己手动添加整个配置。那么根据签名介绍的三个步骤,依葫芦画瓢,就可以添加了。

    这里刚好就用添加 FFMpeg so 到本系列文章现有 Demo 工程中来演示一遍。

    二、引入 FFmpeg so

    1. 新建 cpp 目录

    首先,在 app/src/main/ 目录下,新建文件夹,并命名为 cpp

    接着,在 cpp 目录下,右键 New -> C/C++ Source File ,新建 native-lib.cpp 文件。

    接着,在 cpp 目录下,右键 New -> File ,新建 CMakeLists.txt ,先将上面 IDE 生成的那份代码粘贴进来, FFmpeg的配置在后面详细讲解。

    # CMakeLists.txt
    
    # cmake 最低版本
    cmake_minimum_required(VERSION 3.4.1)
    
    # 配置so库编译信息
    add_library( 
            # 输出so库的名称
            native-lib
    
            # 设置生成库的方式,默认为SHARE动态库
            SHARED
    
            # 列出参与编译的所有源文件
            native-lib.cpp)
    
    # 查找代码中使用到的系统库
    find_library( # Sets the name of the path variable.
            log-lib
    
            # Specifies the name of the NDK library that
            # you want CMake to locate.
            log)
    
    # 指定编译目标库时,cmake要链接的库
    target_link_libraries(
            # 指定目标库,native-lib 是在上面 add_library 中配置的目标库
            native-lib
    
            # 列出所有需要链接的库
            ${log-lib})
    

    2. 将 CMakeLists 配置到 build.gradle 中

    android {
        // ...
        
        defaultConfig {
        // ...
        
        // 1) CMake 编译配置
        externalNativeBuild {
                cmake {
                    cppFlags ""
                }
            }
        }
        
        // ...
        
        // 2) 配置 CMakeLists 路径
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt"
                version "3.10.2"
            }
        }
    }
    
    // ...
    

    如果只是简单的编写 C/C++ 代码,以上基础配置就可以了。

    接着来看看本文的重点,如何使用 CMakeLists.txt 引入 FFmpeg 的动态库。

    3. 将 FFmpeg so 库放到对应的 CPU 架构目录

    上一篇文章中,我们编译的 FFmpeg so 库的 CPU 架构为 armv7-a,所以,我们需要把所有的 so 库放置到 armeabi-v7a 目录下。

    首先,在 app/src/main/ 目录下,新建文件夹,并命名为 jniLibs

    app/src/main/jniLibs 是 Android Studio 默认的放置 so 动态库的目录。

    接着,在 jniLibs 目录下,新建 armeabi-v7a 目录。

    最后把 FFmpeg 编译得到的所有 so 库粘贴到 armeabi-v7a 目录。如下:

    so目录

    4. 添加 FFmpeg so 的头文件

    在编译 FFmpeg 的时候,除了生成 so 外,还会生成对应的 .h 头文件,也就是 FFmpeg 对外暴露的所有接口。

    FFmpeg编译输出

    cpp 目录下,新建 ffmpeg 目录,然后把编译时生成的 include 文件粘贴进来。

    头文件目录

    5. 添加、链接 FFmpeg so 库

    上面已经把 so头文件 放置到对应的目录中了,但是编译器是不会把它们编译、链接、并打包到 Apk 中的,我们还需要在 CMakeLists.txt 中显性的把相关的 so 添加和链接起来。完整的 CMakeLists.txt 如下:

    cmake_minimum_required(VERSION 3.4.1)
    
    # 支持gnu++11
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
    
    # 1. 定义so库和头文件所在目录,方面后面使用
    set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
    set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
    
    # 2. 添加头文件目录
    include_directories(${ffmpeg_head_dir}/include)
    
    # 3. 添加ffmpeg相关的so库
    add_library( avutil
            SHARED
            IMPORTED )
    set_target_properties( avutil
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libavutil.so )
    
    add_library( swresample
            SHARED
            IMPORTED )
    set_target_properties( swresample
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libswresample.so )
            
    add_library( avcodec
            SHARED
            IMPORTED )
    set_target_properties( avcodec
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libavcodec.so )
            
    add_library( avfilter
            SHARED
            IMPORTED)
    set_target_properties( avfilter
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libavfilter.so )
            
    add_library( swscale
            SHARED
            IMPORTED)
    set_target_properties( swscale
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libswscale.so )
    
    add_library( avformat
            SHARED
            IMPORTED)
    set_target_properties( avformat
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libavformat.so )
    
    add_library( avdevice
            SHARED
            IMPORTED)
    set_target_properties( avdevice
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libavdevice.so )
    
    # 查找代码中使用到的系统库
    find_library( # Sets the name of the path variable.
            log-lib
    
            # Specifies the name of the NDK library that
            # you want CMake to locate.
            log )
    
    # 配置目标so库编译信息
    add_library( # Sets the name of the library.
            native-lib
    
            # Sets the library as a shared library.
            SHARED
    
            # Provides a relative path to your source file(s).
            native-lib.cpp
            )
    
    # 指定编译目标库时,cmake要链接的库        
    target_link_libraries( 
    
            # 指定目标库,native-lib 是在上面 add_library 中配置的目标库
            native-lib
    
    # 4. 连接 FFmpeg 相关的库
            avutil
            swresample
            avcodec
            avfilter
            swscale
            avformat
            avdevice
    
            # Links the target library to the log library
            # included in the NDK.
            ${log-lib} )
    

    主要看看注释中新加入的 1~4 点。

    1)通过 set 方法定义了 so头文件 所在目录,方便后面使用。

    其中 CMAKE_SOURCE_DIR 为系统变量,指向 CMakeLists.txt 所在目录。 ANDROID_ABI 也是系统变量,指向 so 对应的 CPU 框架目录:armeabi、armeabi-v7a、x86 ...

    set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
    set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR}/ffmpeg)
    

    2)通过 include_directories 设置头文件查找目录

    include_directories(${ffmpeg_head_dir}/include)
    

    3)通过 add_library 添加 FFmpeg 相关的 so 库,以及 set_target_properties 设置 so 对应的目录。

    其中,add_library 第一个参数为 so 名字,SHARED 表示引入方式为动态库引入。

    add_library( avcodec
            SHARED
            IMPORTED )
    set_target_properties( avcodec
            PROPERTIES IMPORTED_LOCATION
            ${ffmpeg_lib_dir}/libavcodec.so )
    

    4)最后,通过 target_link_libraries 把前面添加进来的 FFMpeg so 库都链接到目标库 native-lib 上。

    这样,我们就将 FFMpeg 相关的 so 库都引入到当前工程中了。下面就要来测试一下,是否可以正常调用到 FFmpeg 相关的方法了。

    三、使用 FFmpeg

    要检查 FFmpeg 是否可以使用,可以通过获取 FFmpeg 基础信息来验证。

    1. 在 FFmpegAcrtivity 中添加一个外部方法 ffmpegInfo

    把获取到的 FFmpeg 信息显示出来。

    class FFmpegActivity: AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_ffmpeg_info)
    
            tv.text = ffmpegInfo()
        }
    
        private external fun ffmpegInfo(): String
    
        companion object {
            init {
                System.loadLibrary("native-lib")
            }
        }
    }
    

    2. 在 native-lib.cpp 中添加对应的 JNI 层方法

    #include <jni.h>
    #include <string>
    #include <unistd.h>
    
    extern "C" {
        #include <libavcodec/avcodec.h>
        #include <libavformat/avformat.h>
        #include <libavfilter/avfilter.h>
        #include <libavcodec/jni.h>
    
        JNIEXPORT jstring JNICALL
        Java_com_cxp_learningvideo_FFmpegActivity_ffmpegInfo(JNIEnv *env, jobject  /* this */) {
    
            char info[40000] = {0};
            AVCodec *c_temp = av_codec_next(NULL);
            while (c_temp != NULL) {
                if (c_temp->decode != NULL) {
                    sprintf(info, "%sdecode:", info);
                    switch (c_temp->type) {
                        case AVMEDIA_TYPE_VIDEO:
                            sprintf(info, "%s(video):", info);
                            break;
                        case AVMEDIA_TYPE_AUDIO:
                            sprintf(info, "%s(audio):", info);
                            break;
                        default:
                            sprintf(info, "%s(other):", info);
                            break;
                    }
                    sprintf(info, "%s[%10s]\n", info, c_temp->name);
                } else {
                    sprintf(info, "%sencode:", info);
                }
                c_temp = c_temp->next;
            }
            return env->NewStringUTF(info);
        }
    }
    

    首先,我们看到代码被包裹在 extern "C" { } 当中,和前面的系统创建的稍微有些不同,通过这个大括号包裹,我们就不需要每个方法都添加单独的 extern "C" 开头了。

    另外,由于 FFmpeg 是使用 C 语言编写的,所在 C++ 文件中引用 #include 的时候,也需要包裹在 extern "C" { },才能正确的编译。

    方法的新建就不用说了,和前面介绍的命名方法一致。

    在方法中,使用 FFmpeg 提供的方法 av_codec_next,获取到 FFmpeg 的编解码器,然后通过循环遍历,将所有的音视频编解码器信息拼接起来,最后返回给 Java 层。

    至此,FFmpeg 加入到工程中,并被调用。

    如果一切正常,App运行后,就会显示出 FFmpeg 音视频编解码器的信息。

    如果由提示 so 或者 头文件 找不到,需要检查 CMakeLists.txt 中设置的 so头文件 的路径是否正确。

    相关文章

      网友评论

        本文标题:【Android 音视频开发打怪升级:FFmpeg音视频编解码篇

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