不知不覺已經(jīng)到了2019年,本系列的文章也更新到了8篇。很慶幸筆者能堅(jiān)持下來,從我司的代碼中學(xué)習(xí)到了很多東西。當(dāng)然更慶幸的是收獲了眾多讀者的鼓勵(lì)和支持。從本篇文章開始,我們將接觸短視頻 app 中比較核心的功能——視頻編輯,筆者在我司的日常工作中,也經(jīng)常對(duì)這個(gè)模塊進(jìn)行開發(fā),可以說對(duì)這部分功能比較熟悉了。所以最近的幾篇文章,我會(huì)從零開始完善一個(gè)視頻編輯 sdk 的各種功能,最后集成到我們之前的 MyTiktok 項(xiàng)目中。注:本文以 android 平臺(tái)為例子,ios 因?yàn)椴粫?huì),所以暫時(shí)不涉及。
本文分為以下章節(jié),讀者可按需閱讀:
1.項(xiàng)目建立——新建一個(gè)跨平臺(tái)視頻編輯項(xiàng)目
2.基礎(chǔ) lib 集成——將 ffmpeg、protobuf 這些必須使用的三方庫集成到項(xiàng)目中
3.基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)——定義和講解一些視頻編輯流程中需要使用到的數(shù)據(jù)結(jié)構(gòu)
我想看本文的人有很大一部分都是 android 工程師,所以在講干貨之前,我需要講一講方法論
1.當(dāng)我們在使用 IDE 開發(fā) App 的時(shí)候,我們在干什么?
1.創(chuàng)建一個(gè)文件夾,模仿 AS 生成的項(xiàng)目,向文件夾里面加文件
2.在 gradle 文件中添加依賴庫,然后寫代碼。使用命令行來 sync gradle。
3.命令行運(yùn)行 gradle 來打包 APK,運(yùn)行 App
1.首先我們會(huì)使用 AS 來新建一個(gè)項(xiàng)目,項(xiàng)目會(huì)有很多可選的參數(shù)。
2.在項(xiàng)目的 gradle 文件中添加依賴庫,然后寫代碼
3.打包成 APK,運(yùn)行 App
1.Android 工程師平時(shí)使用 Android Studio 來開發(fā) App,ios 工程師使用 XCode。那么我們平時(shí)在使用 IDE 的時(shí)候,我們到底在干什么呢?
2.這里我以 Android 來舉例子:
3.上面就是我們平時(shí)開發(fā)的流程了,那么我們能不使用 IDE 來開發(fā)一個(gè) App 嗎?理論上來說是可以的,有下面這些步驟。
4.其實(shí)我們不需要 AS 就能進(jìn)行 Android 開發(fā)(當(dāng)然沒有人那么傻)。我們需要的只是一個(gè)項(xiàng)目管理的工具——gradle。放在不同的開發(fā)者那里,只是項(xiàng)目管理的工具不同:寫 java 的用 maven、寫 python 的用 conda/pip、寫前端的用 npm、寫 c/c++ 的用 CMake。
5.所以當(dāng)一個(gè)項(xiàng)目中既要寫 c++ 又要寫 android 還要寫 ios 的時(shí)候,我們只需要三個(gè)項(xiàng)目管理工具就行了,IDE 對(duì)我們來說只是一個(gè)文件編輯器+文件搜索器+文件瀏覽器。
6.以上就是我作為一個(gè) android 工程師,在使用了各種不同語言構(gòu)建不同項(xiàng)目之后思維上的轉(zhuǎn)變。當(dāng)你能看清和思考一個(gè)東西的本質(zhì)的時(shí)候你能走的更遠(yuǎn)。
那么廢話不多說,就開始搭建我們的項(xiàng)目吧。注意:目前 MyTiktokVideoEditor 已經(jīng)上傳到了 github 上面了,建議結(jié)合項(xiàng)目食用,
1.首先我們新建一個(gè)文件夾,然后進(jìn)入文件夾中。在其中創(chuàng)建下面這些東西,如圖1。里面的東西我一個(gè)個(gè)來講解
1.首先 LICENSSE 和 README.md 就不用說了。
2.android:下面是一個(gè)完整的 android 工程,android 工程的內(nèi)部也會(huì)引用到外部的文件,這個(gè)后面再說。
3.ios:下會(huì)是一個(gè)完整的 ios 工程,當(dāng)然我目前還不會(huì) ios,所以先略過
4.buildtools:里面會(huì)存放一些項(xiàng)目運(yùn)行時(shí)的腳本,比如我們在 上一篇文章 中用到的編譯 FFmpeg 的腳本等等
5.docs:內(nèi)部存放一些項(xiàng)目文檔
6.sharedcode:里面存放 android 和 ios 共享的代碼,如 c/c++ 代碼等等,還有就是 protobuf 生成的代碼。
7.sharedproto:里面存放 android、ios、c++ 三端共享的 protobuf 代碼,可以使用 buildtools 里面的腳本一鍵生成三端的代碼
8.third_part:可以以 git submodule 的形式,引用其他的三方庫的源代碼與 android 和 ios 項(xiàng)目一起編譯,目前是空的。
2.介紹好了項(xiàng)目構(gòu)成,我們開始配置 android 項(xiàng)目吧。
1.首先 externalNativeBuild.cmake 里面配置了一些參數(shù),這里只要知道我們使用的是 c++11 就好了
2.externalNativeBuild.ndk 里面我們只生成一種 so 文件就是 armeabi,本來是應(yīng)該使用 arm64-v8a,這樣才是最佳適配,現(xiàn)在就先湊合著用吧
3.再看外面的 externalNativeBuild.cmake,這里設(shè)置了 CMake 的路徑,注意這里是以當(dāng)前 gradle 文件為初始路徑的。
1.多了 jni.editorsdk 目錄,這個(gè)目錄用來存放 jni 文件,相當(dāng)于是 c/c++ 和 java 的中間層。
2.然后是 CMakeLists.txt 文件,其用于管理 android 項(xiàng)目需要引入的 c/c++ 代碼。
1.首先,我們需要使用 AS 來創(chuàng)建一個(gè)支持 C++ 的工程,注意目錄需要選在上面提到的 android 目錄下面。
2.創(chuàng)建好了之后,我們需要?jiǎng)?chuàng)建一個(gè) android library 作為視頻編輯 sdk 的載體。這個(gè) module 將會(huì)整合所有的,共享 cpp 代碼、.so 文件、.a 文件,然后通過 java 代碼被外部調(diào)用。在項(xiàng)目中我將這個(gè) module 命名為了 mttvideoeditorsdk
3.至于 app module 可以引用 mttvideoeditorsdk module 便于平時(shí)調(diào)試 sdk。
4.我們再來看 mttvideoeditorsdk 的結(jié)構(gòu)如圖2,其實(shí)比較簡單
5.我們再來看看 gradle 文件是怎么配置的如圖3。
上面講了如何搭建項(xiàng)目,這一章就來講講如何集成一些基礎(chǔ)庫吧。
首先我們都知道,在 android 中我們可以使用 gradle 向遠(yuǎn)程中央倉庫拉取我們需要的庫。像 java 的 maven、js 的 npm、ios 的 pods都有這個(gè)能力。但是在 c/c++ 上的項(xiàng)目管理工具 CMake 就沒有這個(gè)能力,它只能在本地搜索和集成你已經(jīng)安裝好的庫或者源碼,而且 c/c++ 又不具有跨平臺(tái)能力。所以最終就導(dǎo)致了我們?nèi)绻胧褂?ffmpeg、protobuf 這樣大型的開源項(xiàng)目都需要自己去 clone 源碼然后自己編譯出不同平臺(tái)的庫。
1.說到 FFmpeg 的集成,其實(shí)我在這里,已經(jīng)提到過一些了。我這里就簡單講講。
2.首先我們需要編譯 FFmpeg 的代碼獲取 so 庫和 頭文件,我的這個(gè)項(xiàng)目與上次不同,現(xiàn)在已經(jīng)能編譯出一個(gè)單獨(dú)的 libffmpeg.so 的文件了,大家可以之前拿過來用。
3.然后我們在 android 項(xiàng)目下面新建一個(gè)目錄用來儲(chǔ)存這些東西,如圖4。
4.最后我們看代碼塊1,這里都有注釋比較簡單,就是將 libffmpeg.so 和他的頭文件鏈接到整個(gè)項(xiàng)目中
----代碼塊1,本文發(fā)自簡書、掘金:何時(shí)夕-----cmake_minimum_required(VERSION 3.4.1)# 當(dāng)前文件存在的目錄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 頭文件與靜態(tài)庫的目錄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 這個(gè) 動(dòng)態(tài) library中add_library(libffmpeg SHARED IMPORTED)set_target_properties(libffmpeg PROPERTIES IMPORTED_LOCATION ${FFMPEG_LIB_DIR}/armeabi/libffmpeg.so)# 將 libprotobuf.a 添加到 libprotobuf-lite 這個(gè) 靜態(tài) 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 這個(gè) 動(dòng)態(tài) library 中l(wèi)ist(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})# 將所有頭文件添加到一個(gè)列表中,在最后一起鏈接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 的頭文件放入一個(gè)列表中 ${FFMPEG_LIB_DIR}/include) # 將 ffmpeg 的頭文件放入一個(gè)列表中target_include_directories(mttvideoeditorsdkjni PRIVATE ${SOURCE_DIR_INCLUDE}) # 連接列表中所有的頭文件list(APPEND LINK_LIBRARIES mttvideoeditorsdkjni -landroid libprotobuf-lite libffmpeg) # 將所有的庫添加到一個(gè)列表中,最后一起鏈接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}) # 鏈接所有庫復(fù)制代碼
1.先上腳本看代碼塊2,里面主要是 clone protobuf 的源碼,然后編譯,然后根據(jù)我們前面建立項(xiàng)目的時(shí)候 sharedproto 文件夾里面的 proto 文件來生成 java c++ 的代碼,最后移動(dòng)到 android 項(xiàng)目和 sharedcode 文件夾下。每次更新了 proto 文件就可以運(yùn)行一下這個(gè)腳本。
2.當(dāng)然還得將 protobuf c++ 的庫集成到項(xiàng)目中,如圖5我們新建一個(gè) android_protobuf 的目錄,然后將剛剛編譯生成的 .a 文件與頭文件拷貝到里面去,這里與 ffmpeg 的集成類似。不同的地方在于,protobuf 生成的是 .a 文件,這里需要將其作為靜態(tài)鏈接庫,添加到項(xiàng)目中。詳細(xì)的在代碼塊1中已經(jīng)說明了。
----代碼塊2,本文發(fā)自簡書、掘金:何時(shí)夕-----#!/bin/bashshow_msg() { echo -e "\033[36m$1\033[0m"}show_err() { echo -e "\033[31m$1\033[0m"}# protobuf 的版本v3_0_0="v3.0.0"# 當(dāng)前的目錄script_path=$(cd `dirname $0`; pwd)# protoc 是 protobuf 編譯之后生成的可執(zhí)行文件,可以用來根據(jù) proto 文件生成 java、c++等等代碼protoc_path=$script_path/tools/protoc# protobuf 的源碼地址protoc_src=$script_path/protobuf# 生成的 java 文件需要移動(dòng)到的位置java_target_path="$script_path/../android/mttvideoeditorsdk/src/main"# 生成的 c++ 文件需要移動(dòng)的位置cpp_target_path="$script_path/../sharedcode/editorsdk/generated_protobuf"# 本方法用于執(zhí)行 protobuf 源碼的腳本進(jìn)行編譯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的版本,然后調(diào)用 build_protobuf 進(jìn)行編譯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 buildfi# 刪除之前已經(jīng)生成的 java c++ 文件rm $java_target_path/java/com/whensunset/mttvideoeditorsdk/model/protobuf/*.javarm $cpp_target_path/*.pb.cc $cpp_target_path/*.pb.hcd $script_path/../sharedprotomkdir -p java cpp# 用 protoc 生成 java c++ 文件$protoc_path/$v3_0_0/bin/protoc *.proto --java_out=java --cpp_out=cpp# 將生成的 java c++ 文件移動(dòng)到對(duì)應(yīng)的文件夾下cp -r java $java_target_pathmkdir -p $cpp_target_pathcp cpp/* $cpp_target_pathrm -rf java cpp復(fù)制代碼
最后一章我們來定義一下在一個(gè)視頻編輯過程中,需要用到的數(shù)據(jù)結(jié)構(gòu)。
1.大家可以看見在 sharedproto 文件夾下面有個(gè) editor_model.proto 文件,里面定義了一些我們在未來整個(gè)視頻編輯功能開發(fā)過程中需要用到的數(shù)據(jù)結(jié)構(gòu),如代碼塊3。
2.前面的幾行初始化代碼就不講了,我就按照定義的一個(gè)個(gè)數(shù)據(jù)結(jié)構(gòu)來進(jìn)行講解
1.TimeRange:這個(gè)顧名思義,用于保存一段時(shí)間,單位是秒。是最基礎(chǔ)的數(shù)據(jù)結(jié)構(gòu),比如特效出現(xiàn)的時(shí)間段、視頻被剪裁的段落、貼紙出現(xiàn)的時(shí)間段等等都需要用到它。
2.MediaStreamHolder:我們都知道(如果不知道可以去看看我之前的文章),F(xiàn)Fmpeg 解封裝了一個(gè)視頻文件之后會(huì)得到好幾個(gè)不同的 stream,每一個(gè) stream 可能是一個(gè)視頻數(shù)據(jù)流可能是一個(gè)音頻數(shù)據(jù)流,而這個(gè)數(shù)據(jù)結(jié)構(gòu)就是為了儲(chǔ)存視頻數(shù)據(jù)流的信息。
3.FileHolder:我們解析了一個(gè)多媒體文件的時(shí)候,也需要把這個(gè)文件的一些信息存下來,比如:文件的后綴名、文件中每個(gè) stream 的信息、文件中最優(yōu)的視頻流和音頻流的 index等等。這個(gè)時(shí)候就要用到這個(gè)數(shù)據(jù)結(jié)構(gòu)了。
4.Color、AssetType:兩個(gè)工具數(shù)據(jù)結(jié)構(gòu)
5.VideoAsset:表示一個(gè)視頻素材,里面除了有個(gè) FileHolder 來儲(chǔ)存視頻被解析后的信息,還有視頻被剪裁的時(shí)間段、視頻的音量、視頻的速度之類的信息。
6.AudioAsset:與 VideoAsset 類似,表示一個(gè)音頻素材。
7.VideoWorkspace:表示一次視頻編輯的數(shù)據(jù)結(jié)構(gòu),里面有復(fù)數(shù)個(gè)視頻素材和音頻素材,以及一些其他參數(shù)。
8.VideoEncoderType:表示當(dāng)前視頻編輯的過程中,視頻用到的編解碼方式。目前只有使用 FFmpeg 編解碼與使用 android 的 mediaCodec 編解碼這兩種方式。
9.這里的數(shù)據(jù)結(jié)構(gòu)隨著 sdk 開發(fā)的進(jìn)行會(huì)不斷的增加和修改,目前這里定義的只是最的簡單版本,大家有想法可以在評(píng)論區(qū)和我交流。
syntax = "proto3";package sharedcode;option optimize_for = LITE_RUNTIME;option java_package = "com.whensunset.mttvideoeditorsdk.model.protobuf";// 用于保存一段時(shí)間,單位是秒message TimeRange { double start = 1; double duration = 2; uint64 id = 3;}// 一個(gè)多媒體文件的一個(gè)多媒體數(shù)據(jù)流的信息message MediaStreamHolder { // 視頻的長和寬 int32 width = 1; int32 height = 2; // 編解碼器的名稱 string codec_type = 3; // 視頻的旋轉(zhuǎn)角度 int32 rotation = 4; // 視頻像素的格式 int32 pix_format = 5; // 視頻的色彩空間,rgb、yuv 等等 int32 color_space = 6; // 視頻的色彩范圍 int32 color_range = 7; // 視頻的 bit 流 int64 bit_rate = 8;}// 儲(chǔ)存一個(gè)多媒體文件的信息,減少反復(fù)解析的性能消耗message FileHolder { string path = 1; // 文件的后綴名 string format_name = 2; int32 probe_score = 3; // 文件中的多媒體數(shù)據(jù)流的數(shù)量 int32 num_streams = 4; // 文件中的多媒體數(shù)據(jù)流的信息列表 repeated MediaStreamHolder streams = 5; // 文件中多媒體信息流中最優(yōu)的視頻流 int32 video_strema_index = 6; // 文件中多媒體信息流中最優(yōu)的音頻流 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;}// 表示一個(gè)視頻素材message VideoAsset { // 相同表示當(dāng)前素材是同樣的 uint64 asset_id = 1; string asset_path = 2; FileHolder asset_video_file_hodler = 3; // 當(dāng)前素材被剪裁的時(shí)間區(qū)域 repeated TimeRange clipped_time_range = 4; // 視頻的速度 double speed = 5; // 視頻聲音大小 double volume = 6; bool is_reversed = 7;}// 表示一個(gè)音頻的素材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;}// 當(dāng)前視頻編輯流程使用的編解碼方式enum VideoEncoderType { VIDEO_ENCODER_TYPE_FFMPEG_MJPEG = 0; VIDEO_ENCODER_TYPE_MEDIACODEC = 1;}復(fù)制代碼
不知不覺又水了一篇文章^_^,最近的兩篇文章都是代碼多而文字少。不知道大家是不是喜歡這種方式呢?(感覺以前廢話太多了,哈哈)大家有什么建議或者意見希望能在評(píng)論區(qū)提出來。如果文章問題可以指出是哪里,方便我進(jìn)行修改(手動(dòng)@上篇文章中說我文章有錯(cuò)別字的哥們)。最近點(diǎn)贊關(guān)注有點(diǎn)少啊,希望大家看完能隨手點(diǎn)個(gè)贊和關(guān)注,謝謝啦!
聯(lián)系客服