天天看點

編譯Android 使用的 libx264 并使用進行 H.264 編碼

在日常的音視訊開發中,我們經常使用

FFmpeg

,因為它确實好用呀,囊括了各種功能!但是有個很嚴重的問題,如果是編譯在Android和IOS上使用,會造成APP的包很大。可以看我編譯的FFmpeg在Android上的應用程式。

github.com/AnJoiner/FF…

一般的

FFmpeg

編譯之後也會有6~7M左右,再加上編譯一些第三方音視訊處理庫的話(如:fdk-aac、mp3lame、libx264等等),可能達到10多M,這樣就造成了APP的臃腫,是以說對于APP上使用的應用程式應該秉承這樣一個原則:

單一原則 - 一個類隻應該有一個功能,這裡需要引申一下,一個功能隻引入一個三方庫

是以即便FFmpeg很強大,但是如果隻是處理單獨的

H.264

編碼視訊,我們僅僅使用libx264就可以了。這也就是為什麼我在APP上放棄使用

FFmpeg

而選擇編譯

libx264

的原因,盡管也能通過FFmpeg去使用libx264,而且還比單獨使用libx264更友善。

libx264

libx264

是支援H.264編碼算法的一套程式,這套程式裡提供了完整的對視訊裸流處理成H.264壓縮的算法。

注意:我這裡所謂的視訊裸流,不僅僅隻是指常用的

YUV420

格式,還有一些其他格式,這裡粘貼一下

libx264

主要支援的視訊裸流格式:

  • X264_CSP_I420

    - YUV420
  • X264_CSP_NV21

    - YUV420格式的一種,但是帶一個y planar 和 一個打包帶v+u,這種格式在Android上就是Camera的資料。
  • X264_CSP_I422

    - YUV422
  • X264_CSP_I444

    - YUV444
  • X264_CSP_RGB

    - RGB格式

還有很多其他格式,基本支援市面上常用的所有格式,如果對于YUV不熟悉的童鞋可以看一下之前的《Android音視訊開發:踩一踩“門檻”》

那接下來我們就來試試,如何将libx264交叉編譯到

Android

上,以及使用編譯的連結檔案進行編碼。

交叉編譯

想要使用libx264我們得編譯成在Android和IOS上能夠使用的二進制檔案:

  • 字尾為.a格式的靜态檔案
  • 字尾為.so格式的動态檔案
注意:這裡編譯是在Linux和MacOS上執行,在Windows配置實在是比較麻煩,真心不如使用虛拟機安裝一個ubuntu的Linux系統。

下載下傳

下載下傳的方式大概有如下兩種:

  • 可以直接官網的下載下傳位址直接進行下載下傳。
  • 也可以打開Terminal,通過git将代碼拷貝到本地
git clone https://code.videolan.org/videolan/x264.git
           

複制

解壓之後大概的目錄内容如下

編譯Android 使用的 libx264 并使用進行 H.264 編碼

編寫腳本

進入上述解壓之後的x264根目錄,然後建立一個build_x264.sh(可以随便取名字,隻要是.sh的檔案就可)的可執行檔案。然後貼上如下代碼:

#!/bin/bash
# Android ndk位置
ANDROID_NDK=/home/c2yu/developer/android/sdk/ndk/android-ndk-r14b


function build_x264()
{
PREFIX=$(pwd)/android/$ANDROID_ABI
SYSROOT=$ANDROID_NDK/platforms/$ANDROID_API/$ANDROID_ARCH
TOOLCHAIN=$ANDROID_NDK/toolchains/$ANDROID_EABI/prebuilt/linux-x86_64/bin

CROSS_PREFIX=$TOOLCHAIN/$CROSS_COMPILE

echo "Compiling x264 for $ANDROID_ABI"
./configure \
    --prefix=$PREFIX \
    --disable-asm \
    --enable-static \
    --enable-shared \
    --enable-pic \
    --host=$HOST \
    --cross-prefix=$CROSS_PREFIX \
    --sysroot=$SYSROOT \
    
make clean
make -j4
make install
echo "The Compilation of x264 for $ANDROID_ABI is completed"
}

# armeabi-v7a
ANDROID_ABI=armeabi-v7a
ANDROID_API=android-14
ANDROID_ARCH=arch-arm
ANDROID_EABI=arm-linux-androideabi-4.9

HOST=arm-linux-androideabi
CROSS_COMPILE=arm-linux-androideabi-

build_x264

# arm64-v8a
ANDROID_ABI=arm64-v8a
ANDROID_API=android-21
ANDROID_ARCH=arch-arm64
ANDROID_EABI=aarch64-linux-android-4.9

HOST=aarch64-linux-android
CROSS_COMPILE=aarch64-linux-android-

build_x264
           

複制

上述為在Linux上的腳本,需要注意
  • ANDROID_NDK 需要替換成你自己的android ndk路徑。這裡使用的是ndk-14b版本
  • 如果是MacOS需要将TOOLCHAIN後路徑替換成ANDROID_NDK/toolchains/ANDROID_EABI/prebuilt/darwin-x86_64/bin

腳本編輯完成之後儲存,然後在

Terminal

賦予可執行權限并編譯:

cd xxx/x264  // 進入x264目錄
sudo chmod +x build_x264.sh // 賦予可執行權限
./build_x264.sh //開始交叉編譯
           

複制

編譯之後會在目前目錄生成一個

android

檔案夾,打開之後就會看到大概如下内容:

編譯Android 使用的 libx264 并使用進行 H.264 編碼

任意打開一個檔案夾,我們可以看到如下内容:

編譯Android 使用的 libx264 并使用進行 H.264 編碼
  • include

    裡面裝的是頭檔案 - 後面會用到
  • bin

    裡面裝的是x264執行檔案 - 終端使用,不用考慮
  • lib

    裡面裝的就是我們需要的.a和.so二進制檔案 - 最終就是為了它
編譯Android 使用的 libx264 并使用進行 H.264 編碼

二進制檔案

使用

雖然我們已經成功編譯出了libx264的二進制檔案,但是在Android上還是不能直接使用。因為還沒有寫編碼程式。

在Android上使用大概有如下兩種方式:

  • 通過

    cmake

    的方式直接在Android Studio上使用
  • ndk-build

    編譯成可直接使用的動态連結檔案。

下面會詳細介紹這兩種方式的使用方法。

Cmake

  1. 建立一個Android的原生項目(Native)。怎麼建立?請參考《Android音視訊開發:音頻非壓縮編碼和壓縮編碼》,裡面介紹了如何建立一個Native項目。
  2. 建立完成項目之後,将上述提到的

    include

    檔案夾裡面的頭檔案放入

    cpp

    這個檔案夾下
編譯Android 使用的 libx264 并使用進行 H.264 編碼
請先忽略這個

h264-encode

的這個c++檔案,在後面會介紹
  1. 然後在

    main

    目錄下建立

    nativeLibs

    檔案夾,将

    arm64-v8a

    armeabi-v7a

    的兩個檔案夾的内容拷貝進去,如上圖那樣。
  2. 建立

    h264-encode.cpp

    的編碼視訊的處理程式。粘貼代碼如下:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include "android/log.h"
 
extern "C" {
#include "x264.h"
#include "jni.h"
}

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG  , "h264-encode", __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO  , "h264-encode", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR  , "h264-encode", __VA_ARGS__)

extern "C"
JNIEXPORT jint JNICALL
Java_com_coder_x264cmake_X264Encode_encode(
        JNIEnv *env, jobject thiz,
        jint width, jint height,
        jstring yuv_path, jstring h264_path, jint yuv_csp) {
    int ret = 0;
    // TODO: implement encode()
    if (width == 0 || height == 0) {
        LOGE("width or height cannot be zero!");
    }
    const char *yuv_file_path = env->GetStringUTFChars(yuv_path, JNI_FALSE);
    const char *h264_file_path = env->GetStringUTFChars(h264_path, JNI_FALSE);

    if (!yuv_file_path) {
        LOGE("yuv path cannot be null");
        return -1;
    }
    if (!h264_file_path) {
        LOGE("h264 path cannot be null");
        return -1;
    }
    // 打開yuv
    FILE *yuv_file = fopen(yuv_file_path, "rb");
    if (yuv_file == NULL) {
        LOGE("cannot open yuv file");
        return -1;
    }
    FILE *h264_file = fopen(h264_file_path, "wb");
    if (h264_file == NULL) {
        LOGE("cannot open h264 file");
        return -1;
    }
    // 設定x264處理的yuv格式預設為YUV420
    int csp = X264_CSP_I420;
    switch (yuv_csp) {
        case 0:
            csp = X264_CSP_I420;
            break;
        case 1:
            csp = X264_CSP_I422;
            break;
        case 2:
            csp = X264_CSP_I444;
            break;
        default:
            csp = X264_CSP_I420;
    }

    LOGI("the params is success:\n %dx%d %s %s:", width, height, yuv_file_path, h264_file_path);

    int frame_number = 0;
    // 處理h264單中繼資料
    int i_nal = 0;
    x264_nal_t *nal = NULL;
    // x264
    x264_t *h = NULL;
    x264_param_t *param = (x264_param_t *) malloc(sizeof(x264_param_t));
    x264_picture_t *pic_in = (x264_picture_t *) (malloc(sizeof(x264_picture_t)));
    x264_picture_t *pic_out = (x264_picture_t *) (malloc(sizeof(x264_picture_t)));

    // 初始化編碼參數
    x264_param_default(param);
    param->i_width = width;
    param->i_height = height;
    param->i_csp = csp;
    // 配置處理級别
    x264_param_apply_profile(param, x264_profile_names[2]);
    // 通過配置的參數打開編碼器
    h = x264_encoder_open(param);

    x264_picture_init(pic_out);
    x264_picture_alloc(pic_in, param->i_csp, param->i_width, param->i_height);
    // 編碼前每一幀的位元組大小
    int size = param->i_width * param->i_height;

    // 計算視訊幀數
    fseek(yuv_file, 0, SEEK_END);
    switch (csp) {
        case X264_CSP_I444:
            // YUV444
            frame_number = ftell(yuv_file) / (size * 3);
            break;
        case X264_CSP_I422:
            // YUV422
            frame_number = ftell(yuv_file) / (size * 2);
            break;
        case X264_CSP_I420:
            //YUV420
            frame_number = ftell(yuv_file) / (size * 3 / 2);
            break;
        default:
            LOGE("Colorspace Not Support.");
            return -1;
    }
    fseek(yuv_file, 0, SEEK_SET);
    // 循環執行編碼
    for (int i = 0; i < frame_number; i++) {
        switch (csp) {
            case X264_CSP_I444:
                fread(pic_in->img.plane[0], size, 1, yuv_file);
                fread(pic_in->img.plane[1], size, 1, yuv_file);
                fread(pic_in->img.plane[2], size, 1, yuv_file);
                break;
            case X264_CSP_I422:
                fread(pic_in->img.plane[0], size, 1, yuv_file);
                fread(pic_in->img.plane[1], size / 2, 1, yuv_file);
                fread(pic_in->img.plane[2], size / 2, 1, yuv_file);
                break;
            case X264_CSP_I420:
                fread(pic_in->img.plane[0], size, 1, yuv_file);
                fread(pic_in->img.plane[1], size / 4, 1, yuv_file);
                fread(pic_in->img.plane[2], size / 4, 1, yuv_file);
                break;
        }
        pic_in->i_pts = i;
        // 對每一幀執行編碼
        ret = x264_encoder_encode(h, &nal, &i_nal, pic_in, pic_out);
        if (ret < 0) {
            LOGE("x264 encode error");
            return -1;
        }
       LOGI("encode frame:%5d", i);
        // 将編碼資料循環寫入目标檔案
        for (int j = 0; j < i_nal; ++j) {
            fwrite(nal[j].p_payload, 1, nal[j].i_payload, h264_file);
        }
    }

    // 沖刷緩沖區,不執行可能造成資料不完整
    int i = 0;
    while (1) {
        ret = x264_encoder_encode(h, &nal, &i_nal, NULL, pic_out);
        if (ret == 0) {
            break;
        }
        LOGD("flush 1 frame");
        // 将編碼資料循環寫入目标檔案
        for (int j = 0; j < i_nal; ++j) {
            fwrite(nal[j].p_payload, 1, nal[j].i_payload, h264_file);
        }
        i++;
    }

    x264_picture_clean(pic_in);
    x264_encoder_close(h);
    // 釋放配置設定的空間
    free(pic_in);
    free(pic_out);
    free(param);
    // 關閉檔案輸入
    fclose(yuv_file);
    fclose(h264_file);

    return ret;
}
           

複制

這段代碼有點長,而且還是 C 語言相關代碼,讀不懂沒有關系,将上述代碼直接粘貼到你的項目中即可。

注意:
  1. 目前程式隻支援

    YUV420

    YUV422

    以及

    YUV444

    三種裸流處理。
  2. Java_com_coder_x264cmake_X264Encode_encode

    這個意思是指在

    com.coder.x264cmake

    的包名下的

    X264Encode

    類中的

    encode

    的方法。需要對應成你自己的完整路徑。
  3. 配置

    CmakeLists.txt

    ,一定要注意這個的配置,稍微一點點配置錯誤,就會出現如下錯誤
編譯Android 使用的 libx264 并使用進行 H.264 編碼

CmakeLists 配置錯誤

為了防止大家配置錯誤,我将CmakeLists.txt的配置粘貼如下:

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

# Declares and names the project.

project("x264cmake")

set( JNI_LIBS_DIR src/main/nativeLibs)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library(
        h264
        STATIC
        IMPORTED
)
#
set_target_properties(
        h264
        PROPERTIES IMPORTED_LOCATION
        ${CMAKE_SOURCE_DIR}/${JNI_LIBS_DIR}/${CMAKE_ANDROID_ARCH_ABI}/libx264.a
)

include_directories(src/main/cpp)

add_library(
        h264-encode
        SHARED
        src/main/cpp/h264-encode.cpp
)

add_library(
        native-lib
        SHARED
        src/main/cpp/native-lib.cpp
)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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 )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       h264-encode
                       h264
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
           

複制

  1. X264Cmake

    中添加加載二進制庫,并添加編碼方法。
編譯Android 使用的 libx264 并使用進行 H.264 編碼

編碼方法

這樣就可以直接運作了,如果出現錯誤,可以參考 X264Cmake

注意:

X264Cmake

項目中assets目錄下

test.yuv

檔案由于太大,是以無法上傳,可在終端通過下面指令将任意視訊轉為YUV420格式的裸流。
ffmpeg -i input.mp4 test.yuv           

複制

這裡可能就會有人問了:不是說不使用FFmpeg了嗎?你這裡怎麼還自己用上了?

「注意:上文說的是在APP中使用的時候」

還有一個地方需要注意,當我們把mp4的視訊檔案轉為yuv的時候,視訊體積會增大數十倍,打個比方:就是1M的mp4視訊,轉成yuv的視訊裸流後,視訊大小大概是幾百M。而且yuv隻有視訊資料,音頻内容被去掉了。

ndk-build

通過ndk-build的方式,直接編譯成動态連結檔案,可以直接放在jniLibs目錄下以供使用,就行正常的時候引入二進制檔案一樣。不需要再建立原生項目,也不需要配置CmakeLists.txt檔案。

  1. x264

    下建立一個build檔案夾,然後進入

    build

    檔案夾,在建立一個

    jni

    檔案夾。
編譯Android 使用的 libx264 并使用進行 H.264 編碼

建立build檔案

  1. 進入

    jni

    檔案夾,将上面用到的幾個檔案拷貝進目前目錄。
  • x264.h

  • x264_config.h

  • h264-encode.cpp

  1. jni

    目錄下建立

    prebuilt

    檔案夾,将

    Cmake

    方式中

    nativeLibs

    下的内容全部拷入。
編譯Android 使用的 libx264 并使用進行 H.264 編碼
  1. jni

    目錄下建立

    Android.mk

    檔案,并添加如下内容。
LOCAL_PATH := $(call my-dir)


include $(CLEAR_VARS)
LOCAL_MODULE := h264
LOCAL_SRC_FILES := prebuilt/$(TARGET_ARCH_ABI)/lib/libx264.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := h264-encode
LOCAL_SRC_FILES :=h264-encode.cpp
                

# LOCAL_C_INCLUDES := /home/relo/coder/android/plugins/FFmpegCommand/ffmpeg-4.2.1
LOCAL_C_INCLUDES := ./

LOCAL_LDLIBS := -llog -ljnigraphics -lz -landroid -lm -pthread -L$(SYSROOT)/usr/lib
LOCAL_STATIC_LIBRARIES := h264

include $(BUILD_SHARED_LIBRARY)
           

複制

  1. jni

    目錄下建立

    Application.mk

    檔案,并添加如下内容。
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-21
APP_OPTIM := release
APP_STL := stlport_static
           

複制

  1. 最終

    jni

    目錄如下
編譯Android 使用的 libx264 并使用進行 H.264 編碼
  1. 激動人心的時候來到了,編譯我們的動态連結庫

使用

Terminal

進入

jni

目錄。

// 執行ndk-build
~/Library/Android/sdk/ndk/android-ndk-r14b/ndk-build
           

複制

如果出現如下提示就表示成功,如果不成功請在下方評論區留言。

編譯Android 使用的 libx264 并使用進行 H.264 編碼

并且在build目錄下,會出現obj和libs兩個檔案,libs下裝的就是最終編譯成功的動态連結庫。

編譯Android 使用的 libx264 并使用進行 H.264 編碼
注意:使用方式與

Cmake

方式一樣,需要建立在

com.coder.x264cmake

的包名下的

X264Encode

類中的

encode

的方法。通過調用

encode

方法才能使用。

驗證

如果需要驗證你通過 H.264 編碼的視訊是否正确,可以通過 VLC 播放器進行播放。

www.videolan.org/vlc/
編譯Android 使用的 libx264 并使用進行 H.264 編碼

結尾

  1. 以上就是就是編譯libx264并在Android上使用的全過程,如果有不正确地方請指出。
  2. 最後還是像開篇說的那樣,需要用到的一些庫的時候,我們再通過編譯的方式進行添加,這樣可以讓你的APP更加健康。

喜歡的話,就點個贊吧?,感謝支援???。

作者:奇葩AnJoiner

來源:https://juejin.cn/post/6949916105271935007

-- END --