• AndroidStudio 中使用FFMPEG


    1.下载 FFmpeg 源码

    git clone https://git.ffmpeg.org/ffmpeg.git

    这一步可能会花比较长的时间

    2.编译 FFmpeg for Android

    2.1.修改 FFmpeg 的 configure

    由于FFMPEG默认编译出来的动态库文件名的版本号在.so之后(例如“libavcodec.so.5.100.1”),但是android平台不能识别这样文件名,所以我们需要修改FFMPEG生成的动态库的文件名。

    打开 configure 文件,找到:

    SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'  
    LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'  
    SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'  
    SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)'

    修改为

    SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'  
    LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'  
    SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'  
    SLIB_INSTALL_LINKS='$(SLIBNAME)'

    2.2.编写 Android 编译脚本

    #!/bin/sh                                                                                                                                       
    NDK=/home/cent/Android/Sdk/ndk-bundle
    SYSROOT=$NDK/platforms/android-19/arch-arm
    TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
    build_android()
    {
        ./configure 
        --prefix=$PREFIX 
        --enable-shared 
        --disable-static 
        --disable-doc 
        --disable-ffmpeg 
        --disable-ffplay 
        --disable-ffprobe 
        --disable-ffserver 
        --disable-avdevice 
        --disable-doc 
        --disable-symver 
        --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- 
        --target-os=linux 
        --arch=arm 
        --enable-cross-compile 
        --sysroot=$SYSROOT 
        --extra-cflags="-Os -fpic $ADDI_CFLAGS" 
        --extra-ldflags="$ADDI_LDFLAGS" 
        $ADDITIONAL_CONFIGURE_FLAG
        make clean
        make
        make install
    }
    CPU=arm
    PREFIX=$(pwd)/android/$CPU
    ADDI_CFLAGS="-marm"
    build_android

    2.3.编译

     执行上面的脚本编译出我们需要的动态库

    ./build_android.sh

    进入android/$CPU目录可以看到生成的动态库和我们需要的头文件

    .
    └── arm
        ├── include
        │   ├── libavcodec
        │   ├── libavfilter
        │   ├── libavformat
        │   ├── libavutil
        │   ├── libswresample
        │   └── libswscale
        └── lib
            ├── libavcodec-57.so
            ├── libavcodec.so -> libavcodec-57.so
            ├── libavfilter-6.so
            ├── libavfilter.so -> libavfilter-6.so
            ├── libavformat-57.so
            ├── libavformat.so -> libavformat-57.so
            ├── libavutil-55.so
            ├── libavutil.so -> libavutil-55.so
            ├── libswresample-2.so
            ├── libswresample.so -> libswresample-2.so
            ├── libswscale-4.so
            ├── libswscale.so -> libswscale-4.so
            └── pkgconfig

    3.将上一步生成的头文件和库文件导入到Android Studio工程中

    首先新建一个工程,并且勾选 Include C++ Support 即可得到一个基于CMake的模板工程。目录结构如下所示

    .
    ├── app
    │   ├── app.iml
    │   ├── build
    │   │   ├── generated
    │   │   │   ├── res
    │   │   │   └── source
    │   │   ├── intermediates
    │   │   │   ├── blame
    │   │   │   ├── incremental
    │   │   │   ├── manifest
    │   │   │   ├── manifests
    │   │   │   ├── res
    │   │   │   ├── rs
    │   │   │   └── symbols
    │   │   └── outputs
    │   │       └── logs
    │   ├── build.gradle
    │   ├── CMakeLists.txt
    │   ├── CMakeLists.txt~
    │   ├── libs
    │   │   ├── armeabi
    │   │   │   ├── libavcodec-57.so
    │   │   │   ├── libavfilter-6.so
    │   │   │   ├── libavformat-57.so
    │   │   │   ├── libavutil-55.so
    │   │   │   ├── libswresample-2.so
    │   │   │   └── libswscale-4.so
    │   │   └── include
    │   │       ├── libavcodec
    │   │       ├── libavfilter
    │   │       ├── libavformat
    │   │       ├── libavutil
    │   │       ├── libswresample
    │   │       └── libswscale
    │   ├── proguard-rules.pro
    │   └── src
    │       ├── androidTest
    │       │   └── java
    │       ├── main
    │       │   ├── AndroidManifest.xml
    │       │   ├── cpp
    │       │   ├── java
    │       │   └── res
    │       └── test
    │           └── java
    ├── build
    │   ├── android-profile
    │   │   └── profile-2017-03-31-23-04-31-347.rawproto
    │   └── generated
    │       └── mockable-android-25.jar
    ├── build.gradle
    ├── FFMPEGTest.iml
    ├── gradle
    │   └── wrapper
    │       ├── gradle-wrapper.jar
    │       └── gradle-wrapper.properties
    ├── gradle.properties
    ├── gradlew
    ├── gradlew.bat
    ├── local.properties
    └── settings.gradle

    然后将上面编译FFMPEG生成的头文件和动态库拷贝到app/libs目录下,拷贝完后的目录结构如下所示

    ├── app
    │   ├── libs
    │   │   ├── armeabi
    │   │   │   ├── libavcodec-57.so
    │   │   │   ├── libavfilter-6.so
    │   │   │   ├── libavformat-57.so
    │   │   │   ├── libavutil-55.so
    │   │   │   ├── libswresample-2.so
    │   │   │   └── libswscale-4.so
    │   │   └── include
    │   │       ├── libavcodec
    │   │       ├── libavfilter
    │   │       ├── libavformat
    │   │       ├── libavutil
    │   │       ├── libswresample
    │   │       └── libswscale
    │   ├── proguard-rules.pro
    │   └── src

    这样还没完,我当时就是这样直接去编译,然后就踩了一个大坑,APP启动之后一直crash,原因就是没有找到我们在java文件里load的动态库。为什么呢?原因是在编译的时候,我们根本没有将我们的动态库打包到APP中,我们还需要修改app/build.gradle将我们放在libs目录下的动态库打包到APP中去

    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 25
        buildToolsVersion "25.0.2"
        defaultConfig {
            applicationId "com.example.cent.ffmpegtest"
            minSdkVersion 15
            targetSdkVersion 25
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
            sourceSets {
                main {
                    jniLibs.srcDirs = ['libs']
                }
            }
            externalNativeBuild {
                cmake {
                    cppFlags "-frtti -fexceptions"
                }
                ndk{
                    abiFilters "armeabi"
                }
            }
        }
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            }
        }
    
        externalNativeBuild {
            cmake {
                path "CMakeLists.txt"
            }
        }
    }
    
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
            exclude group: 'com.android.support', module: 'support-annotations'
        })
        compile 'com.android.support.constraint:constraint-layout:1.0.1'
        testCompile 'junit:junit:4.12'
    }

    紧接着我们还要指定abiFilters,因为AndroidStudio默认会编译所有架构的动态库,但是在本次例子中,我们实际上只拷贝了

    ├── libs
    │   │   ├── armeabi

    架构(目录名)的动态库,所以我们需要指定一个abiFilters来过滤一下,否则会出现编译错误。

     externalNativeBuild {
                cmake {
                    cppFlags "-frtti -fexceptions"
                }
                ndk{
                    abiFilters "armeabi"
                }
            }
        }

    紧接着就是来编写我们的CMakeLists.txt文件来编译我们的动态库和native源文件了

    cmake_minimum_required(VERSION 3.4.1)
    
    find_library( log-lib
                  log )
    
    set(distribution_DIR ../../../../libs)
    
    add_library( native-lib
                 SHARED
                 src/main/cpp/native-lib.cpp )
    
    add_library( avcodec-57
                 SHARED
                 IMPORTED)
    set_target_properties( avcodec-57
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/armeabi/libavcodec-57.so)
    
    add_library( avfilter-6
                 SHARED
                 IMPORTED)
    set_target_properties( avfilter-6
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/armeabi/libavfilter-6.so)
    
    add_library( avformat-57
                 SHARED
                 IMPORTED)
    set_target_properties( avformat-57
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/armeabi/libavformat-57.so)
    
    add_library( avutil-55
                 SHARED
                 IMPORTED)
    set_target_properties( avutil-55
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/armeabi/libavutil-55.so)
    
    add_library( swresample-2
                 SHARED
                 IMPORTED)
    set_target_properties( swresample-2
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/armeabi/libswresample-2.so)
    
    add_library( swscale-4
                 SHARED
                 IMPORTED)
    set_target_properties( swscale-4
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/armeabi/libswscale-4.so)
    
    include_directories(libs/include)
    
    target_link_libraries( native-lib
                           avcodec-57
                           avfilter-6
                           avformat-57
                           avutil-55
                           swresample-2
                           swscale-4
                           ${log-lib} )

    这样基本上就大功告成了。

    4.使用FFMPEG

    下面我们将通过一个小例子来看一下怎样使用FFMPEG。使用FFMPEG进行视频解码(音频和视频很相似)的一般流程如下图所示

     首先需要在JAVA文件中加载我们需要的动态库

    //MainActivity.java
    public class MainActivity extends Activity {
    
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("native-lib");
            System.loadLibrary("avcodec-57");
            System.loadLibrary("avfilter-6");
            System.loadLibrary("avformat-57");
            System.loadLibrary("avutil-55");
            System.loadLibrary("swresample-2");
            System.loadLibrary("swscale-4");
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // Example of a call to a native method
            TextView tv = (TextView) findViewById(R.id.sample_text);
            tv.setText(stringFromJNI());
    
            String input = new File(Environment.getExternalStorageDirectory(),"input.mp4").getAbsolutePath();
            String output = new File(Environment.getExternalStorageDirectory(),"output_yuv420p.yuv").getAbsolutePath();
            decode(input, output);
        }
    
        /**
         * A native method that is implemented by the 'native-lib' native library,
         * which is packaged with this application.
         */
        public native String stringFromJNI();
        public native static void decode(String input,String output);
    }

    然后在native代码中实现主要逻辑

    //native-lib.cpp
    #include <jni.h>
    #include <string>
    #include <android/log.h>
    
    extern "C" {
        //编码
        #include "libavcodec/avcodec.h"
        //封装格式处理
        #include "libavformat/avformat.h"
        //像素处理
        #include "libswscale/swscale.h"
    }
    
    #define FFLOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"ffmpeg",FORMAT,##__VA_ARGS__);
    #define FFLOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"ffmpeg",FORMAT,##__VA_ARGS__);
    
    
    
    extern "C"
    JNIEXPORT jstring JNICALL
    Java_com_example_cent_ffmpegtest_MainActivity_stringFromJNI(
            JNIEnv *env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_example_cent_ffmpegtest_MainActivity_decode(JNIEnv *env, jclass type, jstring input_,
                                                         jstring output_) {
        //获取输入输出文件名
        const char *input = env->GetStringUTFChars(input_, 0);
        const char *output = env->GetStringUTFChars(output_, 0);
    
        //1.注册所有组件
        av_register_all();
    
        //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
        AVFormatContext *pFormatCtx = avformat_alloc_context();
    
        //2.打开输入视频文件
        if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0)
        {
            FFLOGE("%s","无法打开输入视频文件");
            return;
        }
    
        //3.获取视频文件信息
        if (avformat_find_stream_info(pFormatCtx,NULL) < 0)
        {
            FFLOGE("%s","无法获取视频文件信息");
            return;
        }
    
        //获取视频流的索引位置
        //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
        int v_stream_idx = -1;
        int i = 0;
        //number of streams
        for (; i < pFormatCtx->nb_streams; i++)
        {
            //流的类型
            if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
            {
                v_stream_idx = i;
                break;
            }
        }
    
        if (v_stream_idx == -1)
        {
            FFLOGE("%s","找不到视频流
    ");
            return;
        }
    
        //只有知道视频的编码方式,才能够根据编码方式去找到解码器
        //获取视频流中的编解码上下文
        AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
        //4.根据编解码上下文中的编码id查找对应的解码
        AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
        if (pCodec == NULL)
        {
            FFLOGE("%s","找不到解码器
    ");
            return;
        }
    
        //5.打开解码器
        if (avcodec_open2(pCodecCtx,pCodec,NULL)<0)
        {
            FFLOGE("%s","解码器无法打开
    ");
            return;
        }
    
        //输出视频信息
        FFLOGI("视频的文件格式:%s",pFormatCtx->iformat->name);
        FFLOGI("视频时长:%d", (pFormatCtx->duration)/1000000);
        FFLOGI("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
        FFLOGI("解码器的名称:%s",pCodec->name);
    
        //准备读取
        //AVPacket用于存储一帧一帧的压缩数据(H264)
        //缓冲区,开辟空间
        AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket));
    
        //AVFrame用于存储解码后的像素数据(YUV)
        //内存分配
        AVFrame *pFrame = av_frame_alloc();
        //YUV420
        AVFrame *pFrameYUV = av_frame_alloc();
        //只有指定了AVFrame的像素格式、画面大小才能真正分配内存
        //缓冲区分配内存
        uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
        //初始化缓冲区
        avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);
    
        //用于转码(缩放)的参数,转之前的宽高,转之后的宽高,格式等
        struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt,
                                                    pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
                                                    SWS_BICUBIC, NULL, NULL, NULL);
        int got_picture, ret;
    
        FILE *fp_yuv = fopen(output, "wb+");
    
        int frame_count = 0;
    
        //6.一帧一帧的读取压缩数据
        while (av_read_frame(pFormatCtx, packet) >= 0)
        {
            //只要视频压缩数据(根据流的索引位置判断)
            if (packet->stream_index == v_stream_idx)
            {
                //7.解码一帧视频压缩数据,得到视频像素数据
                ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
                if (ret < 0)
                {
                    FFLOGE("%s","解码错误");
                    return;
                }
    
                //为0说明解码完成,非0正在解码
                if (got_picture)
                {
                    //AVFrame转为像素格式YUV420,宽高
                    //2 6输入、输出数据
                    //3 7输入、输出画面一行的数据的大小 AVFrame 转换是一行一行转换的
                    //4 输入数据第一列要转码的位置 从0开始
                    //5 输入画面的高度
                    sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
                              pFrameYUV->data, pFrameYUV->linesize);
    
                    //输出到YUV文件
                    //AVFrame像素帧写入文件
                    //data解码后的图像像素数据(音频采样数据)
                    //Y 亮度 UV 色度(压缩了) 人对亮度更加敏感
                    //U V 个数是Y的1/4
                    int y_size = pCodecCtx->width * pCodecCtx->height;
                    fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                    fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                    fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);
    
                    frame_count++;
                    FFLOGI("解码第%d帧",frame_count);
                }
            }
    
            //释放资源
            av_free_packet(packet);
        }
    
        fclose(fp_yuv);
    
        av_frame_free(&pFrame);
    
        avcodec_close(pCodecCtx);
    
        avformat_free_context(pFormatCtx);
    
        env->ReleaseStringUTFChars(input_, input);
        env->ReleaseStringUTFChars(output_, output);
    }

    记得在Manifest文件中添加需要的权限

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

    最后简单了解一下FFMPEG中使用的几个主要数据结构的作用

  • 相关阅读:
    各大代码托管服务器的分析比较
    《构建之法》读后
    【转】简单的程序诠释C++ STL算法系列之十五:swap
    【转】error while loading shared libraries: xxx.so.x" 错误的原因和解决办法
    C++大会感悟
    一次DDOS攻击引起的安全漫谈
    为npm设置代理,解决网络问题
    Rust 中的类型转换
    Rust 智能指针(二)
    软件设计原则
  • 原文地址:https://www.cnblogs.com/CoderTian/p/6651343.html
Copyright © 2020-2023  润新知