從零開始仿寫一個抖音App——跨平臺視訊編輯SDK專案搭建

何時夕發表於2019-01-05

本文首發於微信公眾號——世界上有意思的事,搬運轉載請註明出處,否則將追究版權責任。微訊號:a1018998632,交流qq群:859640274

GitHub地址

不知不覺已經到了2019年,本系列的文章也更新到了8篇。很慶幸筆者能堅持下來,從我司的程式碼中學習到了很多東西。當然更慶幸的是收穫了眾多讀者的鼓勵和支援。從本篇文章開始,我們將接觸短視訊 app 中比較核心的功能——視訊編輯,筆者在我司的日常工作中,也經常對這個模組進行開發,可以說對這部分功能比較熟悉了。所以最近的幾篇文章,我會從零開始完善一個視訊編輯 sdk 的各種功能,最後整合到我們之前的 MyTiktok 專案中。注:本文以 android 平臺為例子,ios 因為不會,所以暫時不涉及。

本文分為以下章節,讀者可按需閱讀:

  • 1.專案建立——新建一個跨平臺視訊編輯專案
  • 2.基礎 lib 整合——將 ffmpeg、protobuf 這些必須使用的三方庫整合到專案中
  • 3.基礎資料結構——定義和講解一些視訊編輯流程中需要使用到的資料結構

一、專案建立

1.方法論

我想看本文的人有很大一部分都是 android 工程師,所以在講乾貨之前,我需要講一講方法論

  • 1.當我們在使用 IDE 開發 App 的時候,我們在幹什麼?
    • 1.Android 工程師平時使用 Android Studio 來開發 App,ios 工程師使用 XCode。那麼我們平時在使用 IDE 的時候,我們到底在幹什麼呢?
    • 2.這裡我以 Android 來舉例子:
      • 1.首先我們會使用 AS 來新建一個專案,專案會有很多可選的引數。
      • 2.在專案的 gradle 檔案中新增依賴庫,然後寫程式碼
      • 3.打包成 APK,執行 App
    • 3.上面就是我們平時開發的流程了,那麼我們能不使用 IDE 來開發一個 App 嗎?理論上來說是可以的,有下面這些步驟。
      • 1.建立一個資料夾,模仿 AS 生成的專案,向資料夾裡面加檔案
      • 2.在 gradle 檔案中新增依賴庫,然後寫程式碼。使用命令列來 sync gradle。
      • 3.命令列執行 gradle 來打包 APK,執行 App
    • 4.其實我們不需要 AS 就能進行 Android 開發(當然沒有人那麼傻)。我們需要的只是一個專案管理的工具——gradle。放在不同的開發者那裡,只是專案管理的工具不同:寫 java 的用 maven、寫 python 的用 conda/pip、寫前端的用 npm、寫 c/c++ 的用 CMake。
    • 5.所以當一個專案中既要寫 c++ 又要寫 android 還要寫 ios 的時候,我們只需要三個專案管理工具就行了,IDE 對我們來說只是一個檔案編輯器+檔案搜尋器+檔案瀏覽器。
    • 6.以上就是我作為一個 android 工程師,在使用了各種不同語言構建不同專案之後思維上的轉變。當你能看清和思考一個東西的本質的時候你能走的更遠。

2.專案搭建

那麼廢話不多說,就開始搭建我們的專案吧注意:目前 MyTiktokVideoEditor 已經上傳到了 github 上面了,建議結合專案食用,

  • 1.首先我們新建一個資料夾,然後進入資料夾中。在其中建立下面這些東西,如圖1。裡面的東西我一個個來講解
    • 1.首先 LICENSSE 和 README.md 就不用說了。
    • 2.android:下面是一個完整的 android 工程,android 工程的內部也會引用到外部的檔案,這個後面再說。
    • 3.ios:下會是一個完整的 ios 工程,當然我目前還不會 ios,所以先略過
    • 4.buildtools:裡面會存放一些專案執行時的指令碼,比如我們在 上一篇文章 中用到的編譯 FFmpeg 的指令碼等等
    • 5.docs:內部存放一些專案文件
    • 6.sharedcode:裡面存放 android 和 ios 共享的程式碼,如 c/c++ 程式碼等等,還有就是 protobuf 生成的程式碼。
    • 7.sharedproto:裡面存放 android、ios、c++ 三端共享的 protobuf 程式碼,可以使用 buildtools 裡面的指令碼一鍵生成三端的程式碼
    • 8.third_part:可以以 git submodule 的形式,引用其他的三方庫的原始碼與 android 和 ios 專案一起編譯,目前是空的。

圖1:根目錄

  • 2.介紹好了專案構成,我們開始配置 android 專案吧。
    • 1.首先,我們需要使用 AS 來建立一個支援 C++ 的工程,注意目錄需要選在上面提到的 android 目錄下面。
    • 2.建立好了之後,我們需要建立一個 android library 作為視訊編輯 sdk 的載體。這個 module 將會整合所有的,共享 cpp 程式碼、.so 檔案、.a 檔案,然後通過 java 程式碼被外部呼叫。在專案中我將這個 module 命名為了 mttvideoeditorsdk
    • 3.至於 app module 可以引用 mttvideoeditorsdk module 便於平時除錯 sdk。
    • 4.我們再來看 mttvideoeditorsdk 的結構如圖2,其實比較簡單
      • 1.多了 jni.editorsdk 目錄,這個目錄用來存放 jni 檔案,相當於是 c/c++ 和 java 的中間層。
      • 2.然後是 CMakeLists.txt 檔案,其用於管理 android 專案需要引入的 c/c++ 程式碼。
    • 5.我們再來看看 gradle 檔案是怎麼配置的如圖3。
      • 1.首先 externalNativeBuild.cmake 裡面配置了一些引數,這裡只要知道我們使用的是 c++11 就好了
      • 2.externalNativeBuild.ndk 裡面我們只生成一種 so 檔案就是 armeabi,本來是應該使用 arm64-v8a,這樣才是最佳適配,現在就先湊合著用吧
      • 3.再看外面的 externalNativeBuild.cmake,這裡設定了 CMake 的路徑,注意這裡是以當前 gradle 檔案為初始路徑的。

圖2:mttvideoeditorsdk目錄

圖3:mttvideoeditorsdk的gradle檔案

二、基礎 lib 整合

上面講了如何搭建專案,這一章就來講講如何整合一些基礎庫吧。

首先我們都知道,在 android 中我們可以使用 gradle 向遠端中央倉庫拉取我們需要的庫。像 java 的 maven、js 的 npm、ios 的 pods都有這個能力。但是在 c/c++ 上的專案管理工具 CMake 就沒有這個能力,它只能在本地搜尋和整合你已經安裝好的庫或者原始碼,而且 c/c++ 又不具有跨平臺能力。所以最終就導致了我們如果想使用 ffmpeg、protobuf 這樣大型的開源專案都需要自己去 clone 原始碼然後自己編譯出不同平臺的庫。

1.FFmpeg 整合

  • 1.說到 FFmpeg 的整合,其實我在這裡,已經提到過一些了。我這裡就簡單講講。
  • 2.首先我們需要編譯 FFmpeg 的程式碼獲取 so 庫和 標頭檔案,我的這個專案與上次不同,現在已經能編譯出一個單獨的 libffmpeg.so 的檔案了,大家可以之前拿過來用。
  • 3.然後我們在 android 專案下面新建一個目錄用來儲存這些東西,如圖4。
  • 4.最後我們看程式碼塊1,這裡都有註釋比較簡單,就是將 libffmpeg.so 和他的標頭檔案連結到整個專案中

圖4:android_ffmpeg目錄

----程式碼塊1,本文發自簡書、掘金:何時夕-----
cmake_minimum_required(VERSION 3.4.1)

# 當前檔案存在的目錄
set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
# MyTiktokVideoEditor 的根目錄
set(ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../..)
# ffmpeg 的目錄
set(FFMPEG_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../android_ffmpeg)
# protobuf 標頭檔案與靜態庫的目錄
set(PROTOBUF_LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../android_protobuf)
# android 專用 c++ 程式碼的目錄
set(EDITORSDK_JNI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/editorsdk)
# c++ 共享程式碼的目錄
set(SHARED_CODE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../../../sharedcode)
# c++ 的版本
set(CMAKE_CXX_STANDARD 11)

# 找到 android ndk 的 log 庫
find_library(log-lib log)

# 將 libffmpeg.so 新增到 libffmpeg 這個 動態 library中
add_library(libffmpeg SHARED IMPORTED)
set_target_properties(libffmpeg PROPERTIES IMPORTED_LOCATION
        ${FFMPEG_LIB_DIR}/armeabi/libffmpeg.so)

# 將 libprotobuf.a 新增到 libprotobuf-lite 這個 靜態 library 中
add_library(libprotobuf-lite STATIC IMPORTED)
set_target_properties(libprotobuf-lite PROPERTIES IMPORTED_LOCATION
        ${PROTOBUF_LIB_DIR}/armeabi/libprotobuf-lite.a)

aux_source_directory(${SOURCE_DIR} SOURCE_DIR_ROOT)
# 將所有自己寫的 c++ 程式碼新增到 mttvideoeditorsdkjni 這個 動態 library 中
list(APPEND SOURCE_DIR_ROOT
        ${EDITORSDK_JNI_DIR}/native-lib.cc
        ${EDITORSDK_JNI_DIR}/ffmpeg_sample_six.cpp)
list(APPEND SOURCE_DIR_ROOT
        ${SHARED_CODE_DIR}/editorsdk/base/av_utils.cc
        ${SHARED_CODE_DIR}/editorsdk/generated_protobuf/editor_model.pb.cc)
add_library(mttvideoeditorsdkjni
            SHARED
            ${SOURCE_DIR_ROOT})
# 將所有標頭檔案新增到一個列表中,在最後一起連結
list(APPEND SOURCE_DIR_INCLUDE
        ${SHARED_CODE_DIR}/editorsdk/base/av_utils.h
        ${SHARED_CODE_DIR}/editorsdk/base/blocking_queue.h
        ${SHARED_CODE_DIR}/editorsdk/generated_protobuf/editor_model.pb.h
        ${PROTOBUF_LIB_DIR}/include # 將 protobuf 的標頭檔案放入一個列表中
        ${FFMPEG_LIB_DIR}/include) # 將 ffmpeg 的標頭檔案放入一個列表中

target_include_directories(mttvideoeditorsdkjni PRIVATE ${SOURCE_DIR_INCLUDE}) # 連線列表中所有的標頭檔案

list(APPEND LINK_LIBRARIES
        mttvideoeditorsdkjni
        -landroid
        libprotobuf-lite
        libffmpeg) # 將所有的庫新增到一個列表中,最後一起連結
target_compile_options(mttvideoeditorsdkjni PUBLIC -D_LIBCPP_HAS_THREAD_SAFETY_ANNOTATIONS -Wthread-safety -Werror -Wall -Wno-documentation -Wno-shorten-64-to-32 -Wno-nullability-completeness)
target_link_libraries(${LINK_LIBRARIES} ${log-lib}) # 連結所有庫

複製程式碼

2.protobuf 整合

  • 1.先上指令碼看程式碼塊2,裡面主要是 clone protobuf 的原始碼,然後編譯,然後根據我們前面建立專案的時候 sharedproto 資料夾裡面的 proto 檔案來生成 java c++ 的程式碼,最後移動到 android 專案和 sharedcode 資料夾下。每次更新了 proto 檔案就可以執行一下這個指令碼。
  • 2.當然還得將 protobuf c++ 的庫整合到專案中,如圖5我們新建一個 android_protobuf 的目錄,然後將剛剛編譯生成的 .a 檔案與標頭檔案拷貝到裡面去,這裡與 ffmpeg 的整合類似。不同的地方在於,protobuf 生成的是 .a 檔案,這裡需要將其作為靜態連結庫,新增到專案中。詳細的在程式碼塊1中已經說明了。
----程式碼塊2,本文發自簡書、掘金:何時夕-----
#!/bin/bash
show_msg() {
  echo -e "\033[36m$1\033[0m"
}

show_err() {
  echo -e "\033[31m$1\033[0m"
}
# protobuf 的版本
v3_0_0="v3.0.0"
# 當前的目錄
script_path=$(cd `dirname $0`; pwd)
# protoc 是 protobuf 編譯之後生成的可執行檔案,可以用來根據 proto 檔案生成 java、c++等等程式碼
protoc_path=$script_path/tools/protoc
# protobuf 的原始碼地址
protoc_src=$script_path/protobuf
# 生成的 java 檔案需要移動到的位置
java_target_path="$script_path/../android/mttvideoeditorsdk/src/main"
# 生成的 c++ 檔案需要移動的位置
cpp_target_path="$script_path/../sharedcode/editorsdk/generated_protobuf"

# 本方法用於執行 protobuf 原始碼的指令碼進行編譯
build_protobuf() {
  mkdir -p $protoc_src/host
  mkdir -p $protoc_path/$1
  cd $protoc_src/host
  ../configure --prefix=$protoc_path/$1 && make -j8 && make install

  if test $? != 0; then
    show_err "Build protobuf failed"
    exit 1
  fi

  cd $script_path
  rm -rf $protoc_src/host
}

# 本方法用於 clone protobuf 的原始碼,然後 checkout 到3.0.0的版本,然後呼叫 build_protobuf 進行編譯
build() {
  git clone https://github.com/google/protobuf.git

  show_msg "Building android protobuff source code"
  cd protobuf
  git checkout $v3_0_0
  git cherry-pick bba446b  # fix issue https://github.com/google/protobuf/issues/2063
  ./autogen.sh
  build_protobuf $v3_0_0

  show_msg "Build protobuf complete"
  cd $script_path
  rm -rf protobuf
}

# 如果 protoc 不存在,那麼就去 clone protobuf 的原始碼,然後編譯
if [ ! -x "$protoc_path/$v3_0_0/bin/protoc" ]; then
  build
fi

# 刪除之前已經生成的 java c++ 檔案
rm $java_target_path/java/com/whensunset/mttvideoeditorsdk/model/protobuf/*.java
rm $cpp_target_path/*.pb.cc $cpp_target_path/*.pb.h

cd $script_path/../sharedproto

mkdir -p java cpp

# 用 protoc 生成 java c++ 檔案
$protoc_path/$v3_0_0/bin/protoc *.proto --java_out=java --cpp_out=cpp

# 將生成的 java c++ 檔案移動到對應的資料夾下
cp -r java $java_target_path
mkdir -p $cpp_target_path
cp cpp/* $cpp_target_path
rm -rf java cpp
複製程式碼

圖5:android_protobuf目錄

三、基礎資料結構

最後一章我們來定義一下在一個視訊編輯過程中,需要用到的資料結構。

  • 1.大家可以看見在 sharedproto 資料夾下面有個 editor_model.proto 檔案,裡面定義了一些我們在未來整個視訊編輯功能開發過程中需要用到的資料結構,如程式碼塊3
  • 2.前面的幾行初始化程式碼就不講了,我就按照定義的一個個資料結構來進行講解
    • 1.TimeRange:這個顧名思義,用於儲存一段時間,單位是秒。是最基礎的資料結構,比如特效出現的時間段、視訊被剪裁的段落、貼紙出現的時間段等等都需要用到它。
    • 2.MediaStreamHolder:我們都知道(如果不知道可以去看看我之前的文章),FFmpeg 解封裝了一個視訊檔案之後會得到好幾個不同的 stream,每一個 stream 可能是一個視訊資料流可能是一個音訊資料流,而這個資料結構就是為了儲存視訊資料流的資訊。
    • 3.FileHolder:我們解析了一個多媒體檔案的時候,也需要把這個檔案的一些資訊存下來,比如:檔案的字尾名、檔案中每個 stream 的資訊、檔案中最優的視訊流和音訊流的 index等等。這個時候就要用到這個資料結構了。
    • 4.Color、AssetType:兩個工具資料結構
    • 5.VideoAsset:表示一個視訊素材,裡面除了有個 FileHolder 來儲存視訊被解析後的資訊,還有視訊被剪裁的時間段、視訊的音量、視訊的速度之類的資訊。
    • 6.AudioAsset:與 VideoAsset 類似,表示一個音訊素材。
    • 7.VideoWorkspace:表示一次視訊編輯的資料結構,裡面有複數個視訊素材和音訊素材,以及一些其他引數。
    • 8.VideoEncoderType:表示當前視訊編輯的過程中,視訊用到的編解碼方式。目前只有使用 FFmpeg 編解碼與使用 android 的 mediaCodec 編解碼這兩種方式。
    • 9.這裡的資料結構隨著 sdk 開發的進行會不斷的增加和修改,目前這裡定義的只是最的簡單版本,大家有想法可以在評論區和我交流。
syntax = "proto3";
package sharedcode;
option optimize_for = LITE_RUNTIME;
option java_package = "com.whensunset.mttvideoeditorsdk.model.protobuf";

// 用於儲存一段時間,單位是秒
message TimeRange {
    double start = 1;
    double duration = 2;
    uint64 id = 3;
}

// 一個多媒體檔案的一個多媒體資料流的資訊
message MediaStreamHolder {
    // 視訊的長和寬
    int32 width = 1;
    int32 height = 2;
    // 編解碼器的名稱
    string codec_type = 3;
    // 視訊的旋轉角度
    int32 rotation = 4;
    // 視訊畫素的格式
    int32 pix_format = 5;
    // 視訊的色彩空間,rgb、yuv 等等
    int32 color_space = 6;
    // 視訊的色彩範圍
    int32 color_range = 7;
    // 視訊的 bit 流
    int64 bit_rate = 8;
}

// 儲存一個多媒體檔案的資訊,減少反覆解析的效能消耗
message FileHolder {
    string path = 1;
    // 檔案的字尾名
    string format_name = 2;
    int32 probe_score = 3;
    // 檔案中的多媒體資料流的數量
    int32 num_streams = 4;
    // 檔案中的多媒體資料流的資訊列表
    repeated MediaStreamHolder streams = 5;
    // 檔案中多媒體資訊流中最優的視訊流
    int32 video_strema_index = 6;
    // 檔案中多媒體資訊流中最優的音訊流
    int32 audio_strema_index = 7;
    string video_comment = 8;
}

message Color {
    float red = 1;
    float green = 2;
    float blue = 3;
    float alpha = 4;
}

// 素材的種類
enum AssetType {
    ASSET_TYPE_VIDEO = 0;
    ASSET_TYPE_AUDIO = 1;
}
// 表示一個視訊素材
message VideoAsset {
    // 相同表示當前素材是同樣的
    uint64 asset_id = 1;
    string asset_path = 2;
    FileHolder asset_video_file_hodler = 3;
    // 當前素材被剪裁的時間區域
    repeated TimeRange clipped_time_range = 4;
    // 視訊的速度
    double speed = 5;
    // 視訊聲音大小
    double volume = 6;
    bool is_reversed = 7;
}

// 表示一個音訊的素材
message AudioAsset {
    uint64 asset_id = 1;
    string asset_path = 2;
    FileHolder asset_audio_file_holder = 3;
    repeated TimeRange clipped_time_range = 4;
    double speed = 5;
    double volume = 6;
    bool is_repeat = 7;
}

// 表示一次視訊編輯的流程
message VideoWorkspace {
    int64 work_space_id = 1;
    repeated VideoAsset video_asset = 2;
    repeated AudioAsset audio_asset = 3;
    repeated TimeRange clipped_ranges = 4;
    int32 workspace_output_width = 5;
    int32 workspace_output_height = 6;
    VideoEncoderType video_encoder_type = 7;
}

// 當前視訊編輯流程使用的編解碼方式
enum VideoEncoderType {
    VIDEO_ENCODER_TYPE_FFMPEG_MJPEG = 0;
    VIDEO_ENCODER_TYPE_MEDIACODEC = 1;
}
複製程式碼

四、結束

不知不覺又水了一篇文章^_^,最近的兩篇文章都是程式碼多而文字少。不知道大家是不是喜歡這種方式呢?(感覺以前廢話太多了,哈哈)大家有什麼建議或者意見希望能在評論區提出來。如果文章問題可以指出是哪裡,方便我進行修改(手動@上篇文章中說我文章有錯別字的哥們)。最近點贊關注有點少啊,希望大家看完能隨手點個贊和關注,謝謝啦!

連載文章

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、網際網路、程式設計師、計算機程式設計。下面是我的微信公眾號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事

相關文章