Android NDK開發之旅32--云服務器Ubuntu下搭建NDK環境,并編譯FFmpeg

Android NDK開發之旅 目錄

前言

因為在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、新建會話
注意:這里一定要選擇SFTP
3.1.2、連接
連接成功
注意:有文件新增和刪除和修改時,記得點擊刷新按鈕刷新目錄。


4、在云主機Ubuntu 中安裝所需的程序

4.1、安裝vim并對其配置

4.1.1、安裝

輸入命令

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

輸入命令

vim /etc/vim/vimrc
vim安裝成功頁面
4.1.3、配置vimrc
(1)點擊 i 鍵,進入到進入編輯模式
(2)設定光標和行數參數
set nu "顯示行號
set tabstop
set ruler "顯示光標位置
set cursorline "光標高亮顯示
注意:-- 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文件夾

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解壓文件

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


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轉換格式生成的文件


至此,Android調用FFmpeg庫完成。

結語

雖然過程漫長,但我相信大家會漲不少知識。


源碼下載

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







微信號kpioneer

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容