美文网首页软件快捷键
Android NDK开发之旅32--云服务器Ubuntu下搭建

Android NDK开发之旅32--云服务器Ubuntu下搭建

作者: 锐心凌志 | 来源:发表于2018-04-26 13:27 被阅读4次

    前言

    因为在Linux环境下编译FFmpeg生成库和头文件下比较方便,所以接下来主要操作在Linux环境下进行。但是对于Android NDK 开发新手来说,自己电脑配置Ubuntu Linux环境过程比较繁琐。而采用云服务器极大的方便了此过程,服务器对客户端远程的支持,让个人开发更加有拓展性和创意性,而且也便利于接下来课程学习。
    现在云服务器发展迅速,有阿里云、腾讯云、百度云、京东云、美团云、网易云等等。开发者可以根据自己的需求选择合适的云服务器。学习与使用云服务器的过程,有利于提升自己个人能力。
    这里我购买了京东云服务器Ubuntu 14.04 64位进行开发。

    1、了解Linux基本操作指令与常用快捷键

    熟悉Linux 基本操作指令与常用快捷键能加快我们的编程速度,在这里我只列举本篇文章中遇到的指令与快捷键,其他的大家可以自己查找资料去学习。

    基本指令与快捷键 解释
    ls 输出当前文件夹下包含的子文件
    mkdir xx 创建文件夹xx
    cd .. 进入到上一级目录
    cd /xx/ 进入到xx目录
    reset 让终端回到预设状态
    clear 清屏
    Tab键 内容联想;比如,该目录下有一个android-ndk-r14b文件夹,先输入一个a,
    再按Tab键就会自动把android-ndk-r14b 文件名输入到命令行中,便于操作。
    touch xxx.xx 新建xxx.xx的文件
    apt-get install xx 在linux系统下安装某个程序
    rm -rf xx 删除文件夹xx和及其目录下所有文件

    2、云服务器Ubuntu基本配置

    了解完Linux基本操作指令与常用快捷键,现在我们进行云服务器Ubuntu基本配置。

    2.1、下载Xshell和Xftp,用来远程控制云服务器。

    Xshell是一个强大的安全终端模拟软件,方便操作命令行。
    Xftp是一个基于 MS windows 平台的功能强大的SFTP、FTP 文件传输软件,方便传输文件。

    2.2、Xshell连接云服务器

    2.2.1、新建会话
    (1)文件->新建

    输入云服务器对应的公网IP地址

    填写云服务器公网Ip地址
    (2)属性->用户身份验证

    输入云服务器设置的用户名和密码

    设置用户名和密码
    2.2.2、连接
    连接成功

    3.1、xftp连接云服务器

    3.1.1、新建会话
    image
    注意:这里一定要选择SFTP
    3.1.2、连接
    image
    注意:有文件新增和删除和修改时,记得点击刷新按钮刷新目录。

    4、在云主机Ubuntu 中安装所需的程序

    4.1、安装vim并对其配置

    4.1.1、安装

    输入命令

    apt-get install vim /etc/vim/vimrc-gtk
    
    
    image
    4.1.2、查看vim安装成功

    输入命令

    vim /etc/vim/vimrc
    
    
    image
    4.1.3、配置vimrc
    (1)点击 i 键,进入到进入编辑模式
    (2)设定光标和行数参数
    set nu "显示行号
    set tabstop
    set ruler "显示光标位置
    set cursorline "光标高亮显示
    
    
    image
    注意:-- INSERT --表示现在是编辑模式
    (3)保存退出
    按Esc键  再次进入命令模式
    shift + : 再输入 x 保存退出
    或shift + z z 保存退出
    
    
    其他快捷键的使用
    shift + : 再输入 q! 强制退出
    
    命令模式下,x 表示删除,dd 表示删除行
    
    

    4.2、安装 dos2unix 将DOS格式的文本文件转换成UNIX格式

    apt-get install dos2unix
    
    

    4.3、安装make用来完成编译工作

    apt-get install make
    
    

    4.4、安装unzip用来解压zip文件包

    apt-get install unzip 
    
    

    5、搭建NDK环境

    5.1、下载Linux版本的NDK

    我下载的是android-ndk-r14b-linux-x86_64.zip这个文件,大家则根据自己项目需要下载。

    5.2、在\目录下创建一个usr文件夹

    image
    cd /
    mkdir usr
    
    
    创建了usr文件夹
    说明:有些开发者问我,usr之外的文件是怎么来的?这些文件是你买云服务的时候,已经配置好的。大家可以先不用管。

    5.3、在usr目录中创建NDK目录,通过Xftp上传已下载好压缩文件

    mkdir NDK
    
    

    5.4、 赋予ndk文件夹下所有文件的drwx权限,使其可执行解压操作。

     chmod 777 -R ndk
    
    
    权限操作

    5.5、解压上传的的zip文件

    解压zip文件到ndk文件下

    unzip android-ndk-r14b-linux-x86_64.zip
    
    
    注意:解压过程比较长属于正常情况
    解压结果

    5.6、配置NDK环境变量

    使用命令vim ~/.bashrc 进入到环境变量配置文件进行编辑 (~代表用户),添加

    export NDKROOT=/usr/ndk/android-ndk-r14b
    export PATH=$NDKROOT:$PATH
    
    
    配置环境变量

    5.7、更新环境变量

    使用命令source ~/.bashrc更新环境变量,ndk-build -v 查看是否配置成功

    环境变量配置成功

    至此,NDK环境已经搭建好了。

    6、编译FFmpeg

    6.1、到FFmpeg官网下载FFmpge. zip

    我这里使用FFmpeg 2.6.9版本,建议大家用2.8以下版本,出现问题便于解决。

    6.2、上传FFmpeg文件并解压

    使用xftp上传ffmpeg压缩包,使用命令unzip ffmpeg-2.6.9.zip解压文件

    image

    6.3、修改 ffmpeg-2.6.9 目录下的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)'
    
    
    注意:如果不进行这一步操作,将生成以.56、.5、.3等结尾的库,这种库Android很难加载到,我们需要的是后缀.so结尾的库。

    6.4、编写shell脚本文件并将其放在ffmpeg-2.6.9目录下

    build_android.sh文件:

    #!/bin/bash
    make clean
    export NDK=/usr/ndk/android-ndk-r14b  
    export SYSROOT=$NDK/platforms/android-9/arch-arm/
    export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
    export CPU=arm
    export PREFIX=$(pwd)/android/$CPU
    export ADDI_CFLAGS="-marm"
    
    function build_one
    {
    ./configure --target-os=linux \
    --prefix=$PREFIX --arch=arm \
    --disable-doc \
    --enable-shared \
    --disable-static \
    --disable-yasm \
    --disable-symver \
    --enable-gpl \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-ffserver \
    --disable-doc \
    --disable-symver \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    --enable-cross-compile \
    --sysroot=$SYSROOT \
    --extra-cflags="-Os -fpic $ADDI_CFLAGS" \
    --extra-ldflags="$ADDI_LDFLAGS" \
    $ADDITIONAL_CONFIGURE_FLAG
    make clean
    make  -j4
    make install
    }
    
    

    指定NDK路径:export NDK=/usr/ndk/android-ndk-r14b ;
    配置CPU架构类型:export CPU=arm,PREFIX是指定动态库输出的路径,然后disable一些不需要的库(可减小输出的动态库的大小);
    enable-shared:生成共享库。

    注意:
    • 换行的时候需要有\,注意不要有额外的空格,否则编译出错
    • 脚本文件统一转为UTF-8无BOM格式。可通过note pad++进行转码,或者先由Linux创建文件再由Windows编辑。
    • NDK尽量不要使用太新的版本,一般使用Android-9即可,否则会出现在Android编译不兼容老版本而无法使用的问题。
    • 将编写好的shell脚本放在解压后的ffmpeg-2.6.9文件夹中。

    6.5、build_android.sh给予执行权限。

    chmod 777 -R build_android.sh
    
    

    6.6、执行文件build_android

    ./build_android.sh
    
    
    执行结束页面
    说明:

    如果出现问题:bad interpreter : No such file or directory,原因:没有将文件转成Linux编码格式。
    转换Linux编码格式有两种方式:

    1、在Linux下创建这个文件touch build_android.sh,从Linux传出到桌面,然后把脚本命令拷入这个文件中,再重新上传到Linux;
    2、使用 dos2unix build_android.sh 转成Linux编码格式
    
    

    6.7、用xftp查看生成文件

    我们发现在ffmpeg-2.6.9文件夹生成android文件夹,在android文件夹下生成arm文件夹

    生成的include和lib include目录 lib目录

    至此,FFmpeg库已编译完成。

    7、利用编译好的FFmpeg库写一个视频解码Demo

    7.1、jni Java声明

    public class VideoUtils {
    
        public native static void decode(String input,String output);
    
        static{
            System.loadLibrary("avutil-54");
            System.loadLibrary("swresample-1");
            System.loadLibrary("avcodec-56");
            System.loadLibrary("avformat-56");
            System.loadLibrary("swscale-3");
            System.loadLibrary("postproc-53");
            System.loadLibrary("avfilter-5");
            System.loadLibrary("avdevice-56");
            System.loadLibrary("myffmpeg");
        }
    }
    
    

    7.2、编写C主程序

    jni目录
    ffmpeg_player.c
    #include <com_haocai_ffmpegtest_VideoUtils.h>
    
    #include <android/log.h>
    
    //解码
    #include "include/libavcodec/avcodec.h"
    //封装格式处理
    #include "include/libavformat/avformat.h"
    //像素处理
    #include "include/libswscale/swscale.h"
    
    #define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"FFmpeg",FORMAT,##__VA_ARGS__);
    #define LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,"FFmpeg",FORMAT,##__VA_ARGS__);
    
    JNIEXPORT void JNICALL Java_com_haocai_ffmpegtest_VideoUtils_decode
      (JNIEnv *env, jclass jcls, jstring input_jstr, jstring output_jstr){
        //需要转码的视频文件(输入的视频文件)
        const char* input_cstr = (*env)->GetStringUTFChars(env,input_jstr,NULL);
        const char* output_cstr = (*env)->GetStringUTFChars(env,output_jstr,NULL);
    
        //1.注册所有组件
        av_register_all();
    
        //封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
        AVFormatContext *pFormatCtx = avformat_alloc_context();
    
        //2.打开输入视频文件
        if (avformat_open_input(&pFormatCtx, input_cstr, NULL, NULL) != 0)
        {
            LOGE("%s","无法打开输入视频文件");
            return;
        }
    
        //3.获取视频文件信息
        if (avformat_find_stream_info(pFormatCtx,NULL) < 0)
        {
            LOGE("%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)
        {
            LOGE("%s","找不到视频流\n");
            return;
        }
    
        //只有知道视频的编码方式,才能够根据编码方式去找到解码器
        //获取视频流中的编解码上下文
        AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
        //4.根据编解码上下文中的编码id查找对应的解码
        AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
        //(迅雷看看,找不到解码器,临时下载一个解码器)
        if (pCodec == NULL)
        {
            LOGE("%s","找不到解码器\n");
            return;
        }
    
        //5.打开解码器
        if (avcodec_open2(pCodecCtx,pCodec,NULL)<0)
        {
            LOGE("%s","解码器无法打开\n");
            return;
        }
    
        //输出视频信息
        LOGI("视频的文件格式:%s",pFormatCtx->iformat->name);
        LOGI("视频时长:%lld", (pFormatCtx->duration)/1000000);
        LOGI("视频的宽高:%d,%d",pCodecCtx->width,pCodecCtx->height);
        LOGI("解码器的名称:%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_cstr, "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)
                {
                    LOGE("%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++;
                    LOGI("解码第%d帧",frame_count);
                }
            }
    
            //释放资源
            av_free_packet(packet);
        }
    
        fclose(fp_yuv);
    
        (*env)->ReleaseStringUTFChars(env,input_jstr,input_cstr);
        (*env)->ReleaseStringUTFChars(env,output_jstr,output_cstr);
    
        av_frame_free(&pFrame);
    
        avcodec_close(pCodecCtx);
    
        avformat_free_context(pFormatCtx);
      }
    
    

    7.3、写mk文件

    Android.mk
    
    LOCAL_PATH := $(call my-dir)
    
    #ffmpeg lib
    include $(CLEAR_VARS)
    LOCAL_MODULE := avcodec
    LOCAL_SRC_FILES := libavcodec-56.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := avdevice
    LOCAL_SRC_FILES := libavdevice-56.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := avfilter
    LOCAL_SRC_FILES := libavfilter-5.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := avformat
    LOCAL_SRC_FILES := libavformat-56.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := avutil
    LOCAL_SRC_FILES := libavutil-54.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := postproc
    LOCAL_SRC_FILES := libpostproc-53.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := swresample
    LOCAL_SRC_FILES := libswresample-1.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := swscale
    LOCAL_SRC_FILES := libswscale-3.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    #myapp
    include $(CLEAR_VARS)
    LOCAL_MODULE := myffmpeg
    LOCAL_SRC_FILES := ffmpeg_player.c
    LOCAL_C_INCLUDES += $(LOCAL_PATH)/include
    LOCAL_LDLIBS := -llog
    LOCAL_SHARED_LIBRARIES := avcodec avdevice avfilter avformat avutil postproc swresample swscale
    include $(BUILD_SHARED_LIBRARY)
    
    
    Applicatoin.mk
    APP_MODULES := myffmpeg
    APP_ABI := armeabi
    APP_PLATFORM := android-9
    
    

    7.4、Android调用主函数

    public class MainActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        public void mDecode(View btn){
            String input = new File(Environment.getExternalStorageDirectory(),"小苹果.mp4").getAbsolutePath();
            String output = new File(Environment.getExternalStorageDirectory(),"小苹果_out.yuv").getAbsolutePath();
            VideoUtils.decode(input, output);
        }
    }
    
    

    7.5、其它文件配置

    build.gradle中
            ndk{
                moduleName "myffmpeg"
            }
            sourceSets.main{
                jni.srcDirs = []
                jniLibs.srcDir "src/main/libs"
            }
    
    
    AndroidManifest.xml中
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    
    

    7.6 结果运行

    7.6.1、Log输出
    I/FFmpeg: 视频的文件格式:mov,mp4,m4a,3gp,3g2,mj2
    I/FFmpeg: 视频时长:211
    I/FFmpeg: 视频的宽高:720,480
    I/FFmpeg: 解码器的名称:mpeg4
    I/FFmpeg: 解码第1帧
    I/FFmpeg: 解码第2帧
    I/FFmpeg: 解码第3帧
    I/FFmpeg: 解码第4帧
    I/FFmpeg: 解码第5帧
    I/FFmpeg: 解码第6帧
    I/FFmpeg: 解码第7帧
    I/FFmpeg: 解码第8帧
    I/FFmpeg: 解码第9帧
    太多省略......
    
    
    7.6.2、mp4转换格式生成的文件
    image

    至此,Android调用FFmpeg库完成。

    结语

    虽然过程漫长,但我相信大家会涨不少知识。

    源码下载

    Github:https://github.com/kpioneer123/FFmpegTest

    相关文章

      网友评论

        本文标题:Android NDK开发之旅32--云服务器Ubuntu下搭建

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