從零開始仿寫一個抖音App——跨平臺視頻編輯SDK項目搭建

本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。微信號:a1018998632,交流qq羣:859640274前端

GitHub地址

不知不覺已經到了2019年,本系列的文章也更新到了8篇。很慶幸筆者能堅持下來,從我司的代碼中學習到了不少東西。固然更慶幸的是收穫了衆多讀者的鼓勵和支持。從本篇文章開始,咱們將接觸短視頻 app 中比較核心的功能——視頻編輯,筆者在我司的平常工做中,也常常對這個模塊進行開發,能夠說對這部分功能比較熟悉了。因此最近的幾篇文章,我會從零開始完善一個視頻編輯 sdk 的各類功能,最後集成到咱們以前的 MyTiktok 項目中。注:本文以 android 平臺爲例子,ios 由於不會,因此暫時不涉及。java

本文分爲如下章節,讀者可按需閱讀:python

  • 1.項目創建——新建一個跨平臺視頻編輯項目
  • 2.基礎 lib 集成——將 ffmpeg、protobuf 這些必須使用的三方庫集成到項目中
  • 3.基礎數據結構——定義和講解一些視頻編輯流程中須要使用到的數據結構

1、項目創建

1.方法論

我想看本文的人有很大一部分都是 android 工程師,因此在講乾貨以前,我須要講一講方法論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 上面了,建議結合項目食用,ios

  • 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文件

2、基礎 lib 集成

上面講了如何搭建項目,這一章就來說講如何集成一些基礎庫吧。c++

首先咱們都知道,在 android 中咱們可使用 gradle 向遠程中央倉庫拉取咱們須要的庫。像 java 的 maven、js 的 npm、ios 的 pods都有這個能力。可是在 c/c++ 上的項目管理工具 CMake 就沒有這個能力,它只能在本地搜索和集成你已經安裝好的庫或者源碼,並且 c/c++ 又不具備跨平臺能力。因此最終就致使了咱們若是想使用 ffmpeg、protobuf 這樣大型的開源項目都須要本身去 clone 源碼而後本身編譯出不一樣平臺的庫。git

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目錄

3、基礎數據結構

最後一章咱們來定義一下在一個視頻編輯過程當中,須要用到的數據結構。程序員

  • 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;
}
複製代碼

4、結束

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

連載文章

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、互聯網、程序員、計算機編程。下面是個人微信公衆號:世界上有意思的事,乾貨多多等你來看。 shell

世界上有意思的事
相關文章
相關標籤/搜索