美文网首页Android音视频学习
FFmpeg-3.3.1移植到Android平台(Mac编译)

FFmpeg-3.3.1移植到Android平台(Mac编译)

作者: 柒黍 | 来源:发表于2017-08-30 18:04 被阅读1286次

    闲话不多说,我们的目标是:

    1.mac平台编译FFmpeg生成so动态库。
    2.Android通过cmake集成,使用so动态库。

    环境

    mac os: 10.12.6
    ndk: 15.1.4119039
    android studio: 2.3.3
    cmake: cmake_minimum_required(VERSION 3.4.1)

    前置条件:

    1:NDK环境配置

    #编辑本地配置文件,没有自行创建
    vim .bash_profile
    

    顺便把android环境也配置后,后面会用到

    #重新加载配置文件
    source .bash_profile
    #验证是否配置正确
    ndk-build
    
    image.png

    出现上述结果说明已经能找到ndk

    2.下载FFmpeg-3.3.1源码

    image.png

    这个地方点击Sourcecode,要吐槽一下他们的设计,开始以为要点上面的那个按钮,点了半天才反应过来要点下面的字。

    image.png

    解压之后放到一个目录:
    这里目录名称可以随便,我统一都放在ffmpeg下
    /Users/leon/Desktop/ffmpeg


    image.png

    2:Mac编译脚本编写(坑点颇多)

    在目录下新建ffmpeg_build.sh(名称可修改)

    image.png

    此处我用Sublime Text编辑,最好不要用其他平台的文件,比如从window平台复制一份已经写好的脚本文件,可能出现字符编码问题.所以我为了避免麻烦直接在Mac平台重写.

    #!/bin/bash
    # 因为我是在根目录,所以要先进入ffmpeg-3.3.1目录
    cd ffmpeg-3.3.1
    make clean
    # NDK的路径,根据自己的安装位置进行设置
    export NDK=/Users/leon/develop/android/android-sdk-macosx/ndk-bundle
    export SYSROOT=$NDK/platforms/android-16/arch-arm/
    export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
    export CPU=arm
    export PREFIX=$(pwd)/android/$CPU
    export ADDI_CFLAGS="-marm"
    function build_one
    {
    ./configure \
        --prefix=$PREFIX \
        --target-os=linux \
        --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
        --arch=arm \
        --sysroot=$SYSROOT \
        --extra-cflags="-Os -fpic $ADDI_CFLAGS" \
        --extra-ldflags="$ADDI_LDFLAGS" \
        --cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
        --nm=$TOOLCHAIN/bin/arm-linux-androideabi-nm \
        --enable-shared \
        --enable-runtime-cpudetect \
        --enable-gpl \
        --enable-small \
        --enable-cross-compile \
        --disable-debug \
        --disable-static \
        --disable-doc \
        --disable-asm \
        --disable-ffmpeg \
        --disable-ffplay \
        --disable-ffprobe \
        --disable-ffserver \
        --enable-postproc \
        --enable-avdevice \
        --disable-symver \
        --disable-stripping \
    $ADDITIONAL_CONFIGURE_FLAG
    sed -i '' 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h
    sed -i '' 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h
    sed -i '' 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h
    sed -i '' 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h
    sed -i '' 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h
    sed -i '' 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h
    sed -i '' 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h
    sed -i '' 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h
    make clean
    # 这里是定义用几个CPU编译,我用4个,一般在5分钟之内编译完成
    make -j4
    make install
    }
    build_one
    

    这个文件在编译时会出现各种奇怪问题,其中一个坑是在每行语句后面不能有空格.比如

    image.png

    3.修改configure文件(修改前最好backup一份)

    image.png

    修改如下所示:
    大概在3300行,注释前四行掉,然后换成没有注释的

    #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)'
    
    image.png

    这里要修改的目的是因为如果不修改生成so文件的名称大概是 xxxx.so.5.60 ,版本号跟在了.so后面,拿到android下无法正确读取,即使手动改变文件名称也不行.
    修改后版本号是在.so前面,文件是以.so结尾.

    *这里进入此目录,执行configure文件生成一些xx.mk配置文件,主要目的是读取本地编译环境信息并记录。

    ./configure --disable-yasm
    

    4:编译

    激动人心的时刻要到了,大部分时候都是要折腾很久才能爽这一下,不好意思,又要开车了。

    # 命令行到脚本所在目录
    ./ffmpeg_build.sh
    

    此时正确的姿势是这样的

    虽然其中会出现这种情况,但是不必管他

    image.png

    第一次折腾好时,一看表已经凌晨2点,说多了都是泪.

    image.png

    编译结束,在FFmpeg-3.3.1目录中会多一个android目录,这里面就是我们要用到的so

    image.png

    在开头我们定的目标一已经完成,来看一下我们目标二.

    下面开始使用Android Studio创建App工程,并使用以上我们编译生成的动态库,编写一个简单的jni调用ffmpeg播放本地视频文件

    5:创建Android项目

    image.png image.png

    可以先运行一下项目,保证新创建的项目没有问题,这里模拟器不能使用x86架构的,但是arm架构的模拟器又巨慢,我用真机测试

    • main文件目录中新建 jniLibs文件件
    • 把本地编译的include文件夹和so赋值过来
    image.png

    6:编写CMakeLists

    cmake_minimum_required(VERSION 3.4.1)
    add_library( native-ffmpeg
                 SHARED
                 src/main/cpp/native-ffmpeg.cpp )
    find_library( log-lib
                  log )
    find_library( android-lib
                  android )
    set(distribution_DIR ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI})
    add_library( avutil-55
                 SHARED
                 IMPORTED )
    set_target_properties( avutil-55
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libavutil-55.so)
    add_library( swresample-2
                 SHARED
                 IMPORTED )
    set_target_properties( swresample-2
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libswresample-2.so)
    add_library( avcodec-57
                 SHARED
                 IMPORTED )
    set_target_properties( avcodec-57
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libavcodec-57.so)
    add_library( avfilter-6
                 SHARED
                 IMPORTED )
    set_target_properties( avfilter-6
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libavfilter-6.so)
    add_library( swscale-4
                 SHARED
                 IMPORTED )
    set_target_properties( swscale-4
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libswscale-4.so)
    add_library( avdevice-57
                 SHARED
                 IMPORTED )
    set_target_properties( avdevice-57
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libavdevice-57.so)
    add_library( avformat-57
                 SHARED
                 IMPORTED )
    set_target_properties( avformat-57
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libavformat-57.so)
    add_library( postproc-54
                 SHARED
                 IMPORTED )
    set_target_properties( postproc-54
                           PROPERTIES IMPORTED_LOCATION
                           ${distribution_DIR}/libpostproc-54.so)
    set(CMAKE_VERBOSE_MAKEFILE on)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
    include_directories(src/main/cpp)
    include_directories(src/main/jniLibs/include)
    
    target_link_libraries(native-ffmpeg
                          avutil-55       #工具库(大部分需要)
                          swresample-2    #音频采样数据格式转换
                          avcodec-57      #编解码(重要)
                          avfilter-6      #滤镜特效处理
                          swscale-4       #视频像素数据格式转换
                          avdevice-57     #各种设备的输入输出
                          avformat-57     #封装格式处理
                          postproc-54     #后加工
                          ${log-lib}
                          ${android-lib})
    
    

    这里cmake需要看下官方文档了解下

    7:编写JNI文件

    image.png

    1)java控制类:PlayerNative.java

    package com.example.leon.ffmpegandroiddemo;
    
    /**
     * Created by leon
     */
    
    public class PlayerNative {
    
        /**
         * 这里加载有依赖关系
         */
        static {
            System.loadLibrary("avutil-55");
            System.loadLibrary("swresample-2");
            System.loadLibrary("avcodec-57");
            System.loadLibrary("avfilter-6");
            System.loadLibrary("swscale-4");
            System.loadLibrary("avdevice-57");
            System.loadLibrary("avformat-57");
            System.loadLibrary("postproc-54");
            System.loadLibrary("native-ffmpeg");
        }
    
    
        /**
         * 音视频解码播放
         *
         * @param path
         * @param view
         */
        public native static void paly(String path, Object view);
    
        /**
         * 音视频解码停止
         *
         */
        public native static void stop();
    
    }
    

    2)Jni:native-ffmpeg.cpp(可以不用写.h文件)

    #include <jni.h>
    #include <string>
    #include "native-ffmpeg.h"
    #include <android/native_window.h>
    #include <android/native_window_jni.h>
    #include <unistd.h>
    #include <android/log.h>
    
    #define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"TAG",FORMAT,##__VA_ARGS__);
    #define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"TAG",FORMAT,##__VA_ARGS__);
    
    extern "C" {
    #include "libavformat/avformat.h"
    #include "libswscale/swscale.h"
    #include <libavfilter/avfiltergraph.h>
    #include "libavfilter/avfilter.h"
    #include "libavutil/imgutils.h"
    #include "libavutil/avutil.h"
    #include "libavfilter/buffersink.h"
    #include "libavfilter/buffersrc.h"
    #include "libavcodec/avcodec.h"
    }
    
    extern "C"
    /**
     * 播放
     */
    JNIEXPORT void JNICALL Java_com_example_leon_ffmpegandroiddemo_PlayerNative_paly
            (JNIEnv *env, jclass cls, jstring path_, jobject view) {
        LOGE("%s", "play()");
        const char *path = env->GetStringUTFChars(path_, 0);
        //注册所有的编解码器
        av_register_all();
        int ret;
        //封装格式上线文
        AVFormatContext *fmt_ctx = avformat_alloc_context();
        //打开输入流并读取头文件。此时编解码器还没有打开
        if (avformat_open_input(&fmt_ctx, path, NULL, NULL) < 0) {
            return;
        }
        //获取信息
        if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
            return;
        }
        //获取视频流的索引位置
        int video_stream_index = -1;
        for (int i = 0; i < fmt_ctx->nb_streams; i++) {
            if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
                video_stream_index = i;
                LOGE("找到视频流索引位置video_stream_index=%d", video_stream_index);
                break;
            }
        }
        if (video_stream_index == -1) {
            LOGE("未找到视频流索引");
        }
        ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, view);
        if (nativeWindow == NULL) {
            LOGE("ANativeWindow_fromSurface error");
            return;
        }
        //绘制时候的缓冲区
        ANativeWindow_Buffer outBuffer;
        //获取视频流解码器
    
        AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
        avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar);
        AVCodec *avCodec = avcodec_find_decoder(codec_ctx->codec_id);
        //打开解码器
        if ((ret = avcodec_open2(codec_ctx, avCodec, NULL)) < 0) {
            ret = -3;
            return;
        }
        //循环从文件读取一帧压缩数据
        //开始读取视频
        int y_size = codec_ctx->width * codec_ctx->height;
        AVPacket *pkt = (AVPacket *) malloc(sizeof(AVPacket));//分配一个packet
        av_new_packet(pkt, y_size);//分配packet的数据
        AVFrame *yuvFrame = av_frame_alloc();
        AVFrame *rgbFrame = av_frame_alloc();
        // 颜色转换器
        SwsContext *m_swsCtx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
                                              codec_ctx->width,
                                              codec_ctx->height, AV_PIX_FMT_RGBA, SWS_BICUBIC, NULL,
                                              NULL, NULL);
    
    
        LOGE("开始解码");
        int index = 0;
        while (1) {
            if (av_read_frame(fmt_ctx, pkt) < 0) {
                //这里就认为视频读完了
                break;
            }
            if (pkt->stream_index == video_stream_index) {
                //视频解码
                ret = avcodec_send_packet(codec_ctx, pkt);
                if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
                    LOGE("avcodec_send_packet ret=%d", ret);
                    av_packet_unref(pkt);
                    continue;
                }
                //从解码器返回解码输出数据
                ret = avcodec_receive_frame(codec_ctx, yuvFrame);
                if (ret < 0 && ret != AVERROR_EOF) {
                    LOGE("avcodec_receive_frame ret=%d", ret);
                    av_packet_unref(pkt);
                    continue;
                }
                //avcodec_decode_video2(codec_ctx,yuvFrame,&got_pictue,&pkt);
                sws_scale(m_swsCtx, (const uint8_t *const *) yuvFrame->data, yuvFrame->linesize, 0,
                          codec_ctx->height, rgbFrame->data, rgbFrame->linesize);
                //设置缓冲区的属性
                ANativeWindow_setBuffersGeometry(nativeWindow, codec_ctx->width, codec_ctx->height,
                                                 WINDOW_FORMAT_RGBA_8888);
                ret = ANativeWindow_lock(nativeWindow, &outBuffer, NULL);
                if (ret != 0) {
                    LOGE("ANativeWindow_lock error");
                    return;
                }
                av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize,
                                     (const uint8_t *) outBuffer.bits, AV_PIX_FMT_RGBA,
                                     codec_ctx->width, codec_ctx->height, 1);
                //fill_ANativeWindow(&outBuffer,outBuffer.bits,rgbFrame);
                //将缓冲区数据显示到surfaceView
                ret = ANativeWindow_unlockAndPost(nativeWindow);
                if (ret != 0) {
                    LOGE("ANativeWindow_unlockAndPost error");
                    return;
                }
                LOGE("成功显示到缓冲区%d次", ++index);
            }
            av_packet_unref(pkt);
            usleep(1000 * 8);
    
        }
    
        av_frame_free(&rgbFrame);
        avcodec_close(codec_ctx);
        sws_freeContext(m_swsCtx);
        avformat_close_input(&fmt_ctx);
        ANativeWindow_release(nativeWindow);
        env->ReleaseStringUTFChars(path_, path);
        LOGI("解析完成");
    }
    
    /**
     * 暂停
     */
    JNIEXPORT void JNICALL Java_com_example_leon_ffmpegandroiddemo_PlayerNative_stop
            (JNIEnv *env, jclass cls) {
        LOGE("%s", "stop()");
    }
    

    3)其他文件
    MainActivity.java

    package com.example.leon.ffmpegandroiddemo;
    
    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.TextView;
    
    import java.io.File;
    
    public class MainActivity extends AppCompatActivity {
    
    
        private VideoView mVideo;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mVideo = (VideoView) findViewById(R.id.video);
        }
    
    
        public void paly(View view) {
            final String dir = Environment.getExternalStorageDirectory() + File.separator + "Download";
            final String path = dir + File.separator + "1.mp4";
            final VideoView mVideo = (VideoView) findViewById(R.id.video);
            if (new File(path).exists()) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        PlayerNative.paly(path, mVideo.getHolder().getSurface());
                    }
                });
            } else {
                System.out.println("文件不存在");
            }
        }
    
        public void stop(View view) {
            PlayerNative.stop();
        }
    }
    

    VideoView.java

    package com.example.leon.ffmpegandroiddemo;
    
    import android.content.Context;
    import android.graphics.PixelFormat;
    import android.util.AttributeSet;
    import android.view.SurfaceHolder;
    import android.view.SurfaceView;
    
    /**
     *
     * Created by leon
     */
    public class VideoView extends SurfaceView {
        public VideoView(Context context) {
            super(context);
            init();
        }
    
        public VideoView(Context context, AttributeSet attrs) {
            super(context, attrs);
            init();
        }
    
        public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init();
        }
    
        private void init() {
            SurfaceHolder holder = getHolder();
            holder.setFormat(PixelFormat.RGBA_8888);
        }
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:orientation="vertical">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
    
            <Button
                android:id="@+id/btn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="paly"
                android:text="开始" />
    
            <Button
                android:id="@+id/btn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:onClick="stop"
                android:text="结束" />
        </LinearLayout>
    
    
        <com.example.leon.ffmpegandroiddemo.VideoView
            android:id="@+id/video"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp" />
    
    </LinearLayout>
    

    到此我们定的两个目标已经完成,以后会再继续写FFmpeg使用教程。

    最后总结一下各种问题:
    1:cannot locate symbol "atof" referenced by "libavformat-57.so"...的问题

    原因:Android的stdlib.h中atof是内联的
    解决方法:将所有的atof改成strtod
    示例代码:
    char *strpi = "3.1415";
    double dpi;
    dpi = atof(strpi); 修改为: dpi = strtod(strpi, NULL);

    或者脚本文件中定义platforms版本使用低版本,比如
    export SYSROOT=$NDK/platforms/android-16/arch-arm/

    2.cannot locate symbol "log2f" referenced by "libavcodec-57.so"..
    原因: 这个跟ndk与android版本有关。如果使用高版本ndk编译,so会判断系统版本并使用系统log,但实际手机系统版本较低,并没有此方法,所以会报找不到此方法
    解决办法:
    1),使用platforms 低版本
    2),修改 ./libavutil/libm.h里面的定义,不再判断是否已经存在函数。使用重新定义

    //#if !HAVE_LOG2  
    //#undef log2  
    #define log2(x) (log(x) * 1.44269504088896340736)  
    //#endif /* HAVE_LOG2 */  
      
    //#if !HAVE_LOG2F  
    //#undef log2f  
    #define log2f(x) ((float)log2(x))  
    //#endif /* HAVE_LOG2F */  
    

    相关文章

      网友评论

        本文标题:FFmpeg-3.3.1移植到Android平台(Mac编译)

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