本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。交流qq羣:859640274java
你們很久不見,又有一個多月沒有發文章了。不知道還有哪些讀者記得個人 從零開始仿寫抖音App 的系列文章,這個系列的文章已經好久沒有更新了,最後一篇文章是我開始開發 視頻編輯SDK 時寫的。當時踏入到了一個新的領域裏,未知的東西太多了,致使接下來的大半年都沒有更新相關的文章。 可是別覺得我已經放棄了,今天對於我來講是一個值得記念的日子,2019年10月28日 我終於將 視頻編輯SDK 的最簡版本給完成了,我將這個 視頻編輯SDK 命名爲 WSVideoEditor,接下來的一段時間裏我計劃更新 4 篇解析該 SDK 的相關文章,WsVideoEditor 中的代碼我也會隨着文章同步更新。當 SDK 解析完畢以後 從零開始仿寫一個抖音App 系列文章將會踏出最關鍵的一步。linux
本文分爲如下章節,讀者可按需閱讀:android
- 1.項目介紹
- 2.SDK功能介紹
- 3.SDK架構以及運行機制介紹
- 4.VideoDecodeService解析
1、項目介紹
本章我將介紹 WsVideoEditor 項目的基本結構、組織方式以及運行方式。須要你們把項目 clone 下來跟着我一步步來作。ios
1.基本結構
咱們看着圖1,一個個來說:git
- 1.android:顧名思義,這個目錄下是一個 Android 項目,去掉 .gradle、build、.idea 等等 ignore 的文件,咱們主要關注下面這幾個文件夾。
- 1.ffmpeg-cpp:如圖2,這個文件夾中有 FFMPEG 的頭文件與 .so 文件,咱們須要將這個庫集成到咱們的 SDK 中,咱們的 編輯SDK 須要有解碼視頻的能力,解碼分爲硬解和軟解,ffmpeg 就是用於軟解的最強開源庫。至於如何獲得這些東西,我以前寫過一篇 FFMPE食用指南 有興趣的讀者能夠看看。
- 2.protobuf-cpp:這個文件夾與 ffmpeg-cpp 相似,裏面有 Protobuf For Cpp 的頭文件與 .a 文件,由於咱們 Native 與 Android/iOS/Linux 的通訊方式使用的是 Protobuf,因此咱們也須要將 Cpp 層的 Protobuf 集成到咱們的 SDK 中。
- 3.wsvideoeditor-sdk:如圖3,這個文件夾是一個 Android Library 項目,咱們的 編輯SDK 在 Android 端會以一個獨立的 jar 包形式存在。這個目錄下的東西比較多,例如 src 目錄下是 Java 層的一些封裝代碼。jni 目錄下是一些使用了 Android Native Api 的 Cpp 代碼。更詳細的解析,會在後面幾章。
- 4.wsvideoeditor-test:這個文件夾則是一個 Android Application 項目,主要是用於編寫一些測試 編輯 SDK 的代碼。
- 2.buildtools:如圖4,這個目錄下主要存放一些工具腳本,例如目前 build_proto.sh 用於生成 Java 與 Cpp 層的 Protobuf 代碼。
- 3.ios、linux:由於我給 編輯SDK 的定義是一個跨平臺的視頻編輯SDK,因此將來的想法是 iOS 和 Linux 端也能接入咱們的 編輯SDK,目前這兩個目錄裏還啥也沒有:-D。
- 4.sharedcpp:如圖5,這個目錄裏面主要存放與平臺無關的 Cpp 代碼,由於咱們要作的是一個跨平臺的視頻編輯 SDK,因此儘可能多的將與平臺無關的代碼進行共用,是一個明智的選擇。能夠看見裏面的 prebuilt_protobuf 目錄下就有咱們使用 build_proto.sh 生成的 Cpp 文件,這些文件就是能夠共用的。
- 5.sharedproto:這裏存放着咱們定義的 Protobuf 文件。
- 6.thirdparty:這裏存放着一些包含源碼的與平臺無關的三方庫,例如 libyuv。
- 7.CMakeLists.txt:這個文件主要是爲了讓 Clion 可以識別咱們這個整個項目。
2.如何運行項目
- 1.
git clone https://github.com/TheGodsThemselves/WsVideoEditor.git
- 2.NDK 環境須要準備好
- 3.用 Android Studio 打開 WsVideoEditor/android 目錄
- 4.在手機中準備 /sdcard/test.mp4 視頻文件
- 5.運行 wsvideoeditor-test 項目
2、SDK功能介紹
這一章咱們來介紹一下 編輯SDK 目前有的以及將來會有的功能。編輯SDK 的最終形態會和抖音的視頻編輯功能接近,有其餘想法的讀者也能夠在評論區留言或者提 issue。程序員
1.目前有的功能
- 1.開始播放
- 2.暫停播放
- 3.視頻音量調整
- 4.單段視頻播放
- 5.多段視頻播放
- 6.視頻 Seek
- 7.視頻邊緣模糊填充
2.規劃中的功能
- 1.視頻類:
- 1.按時間軸添加額外的聲音
- 2.按時間軸添加濾鏡
- 3.按時間軸添加靜態貼紙、動態貼紙
- 4.多段視頻間轉場
- 2.圖片類:
- 3.工具類:
- 4.編碼類:
- 1.導出不一樣格式的視頻
- 2.更改視頻的分辨率、幀率
- 3.視頻轉 gif
- 5.技術類:
- 1.多進程編解碼視頻
- 2.多進程播放視頻
- 3.多進程視頻縮略圖截取
3、SDK架構以及運行機制介紹
這一章我來介紹一下目前 編輯SDK 的總體架構以及運行機制。github
1.編輯SDK架構
圖6是 編輯SDK 的架構圖,這一節我會照着這張圖來介紹。
(1).基礎API
先從底部看起,底部是整個 SDK 依賴的底層 API 庫。編程
- 1.FFMPEG:前面簡單介紹過,是一個開源的視頻庫,在咱們的項目中主要用於軟編解碼。
- 2.MediaCodec:是 Android 中的硬編解碼 API,相應的 iOS 也有本身的硬編解碼方式。
- 3.OpenGL:是一個開源的圖形庫,Android 和 iOS 中都有內置 OpenGL ES 做爲默認圖形庫。在咱們的項目中主要用於將視頻解碼後的視頻幀繪製到屏幕上去。固然也能夠對這些圖像作一些效果的變化,例如濾鏡、視頻/圖片轉場等等。
- 4.Libyuv:是 Google 開源的實現各類 YUV 與 RGB 之間相互轉換、旋轉、縮放的庫。
- 5.Protobuf:是 Google 開源的一種平臺無關、語言無關、可擴展且輕便高效的序列化數據結構的協議。在咱們的項目中主要用於 Cpp 與 Java、OC、Dart 之間的數據通訊。
(2).SDK主體
接着咱們再看圖片中的主體部分,由於目前只有 Android 端的實現,因此主體部分的上層實現我使用 Android 來代替。後端
- 1.Android層架構:
- 1.WSMediaPlayerView:繼承於 TextureView,因此其能夠提供一個具備 Open GL 環境的線程。對 Surface 家族不瞭解的同窗能夠看看這兩篇文章:Android繪製機制以及Surface家族源碼全解析、相機/OpenGL/視頻/Flutter和SurfaceView
- 2.WSMediaPlayer:這個是一個代理了 Native 的 NativeWSMediaPlayer 的 Java 類。該類具備一個播放器應該有的各類 API,例如 play、pause、seek 等等。其實不少 Android 中的系統類都是以這種形式存在的,例如 Bitmap、Surface、Canvas 等等。說到底 Java 只是 Android 系統方便開發者開發 App 的上層語言,系統中大部分的功能最終都會走到 Native 中去,因此讀者須要習慣這種代碼邏輯
- 3.AudioPlayer:這個類是基於 Android 中的 AudioTrack 封裝的可以播放音頻幀的 Java 類。咱們在 Native 層也有一個 AudioPlayer。這裏與 WSMediaPlayer 相反 Native 層的 AudioPlayer 是一個空殼,Java 層的 AudioPlayer 反向代理了 Native 層的 AudioPlayer,由於在這裏 Java 層的 AudioPlayer 纔是真正播放音頻的東西。
- 2.Native層架構:這裏咱們自底向上來剖析,Native 層的架構
- 1.AudioDecodeService:它負責使用 FFMPEG/MediaCodec,來從視頻/音頻中解碼出某個時間點的音頻幀,而且存儲在一個音頻幀隊列中。最終被外部取出音頻幀交給音頻播放器播放。
- 2.VideoDecodeService:它和 AudioDecodeService 相似,是使用 FFMPEG/MediaCodec 來從視頻中解碼出某個時間點的視頻幀而且存儲在一個視頻幀隊列中。最終被外部取出視頻幀交給 OpenGL 繪製到屏幕上。
- 3.VideoFramePool:它負責響應外部的 seek 事件,而後使用 FFMPEG/MediaCodec 來從視頻中解碼出當前時間點的視頻幀,而後存儲到一個 LruCache 中同時返回 seek 時間點的視頻幀。
- 4.AudioPlayer:前面說過,這個是 Java 層的 AudioPlayer 代理類,主要用於播放 AudioDecodeService 解碼出來音頻幀。
- 5.FrameRenderer:這個東西是一個渲染器,在視頻播放時用於渲染 VideoDecodeService 不斷解碼出的視頻幀,在視頻 seek 的時用於向 VideoDecoderPool 發送 seek 請求,而後渲染返回的視頻幀。
- 6.NativeWSMediaPlayer:用於同步 AudioPlayer 和 FrameRenderer 的音視頻播放。即咱們通常認爲的視頻播放器實體,被 Java 層的 WSMediaPlayer 代理着。
2.編輯SDK運行機制
上一節講解了 編輯SDK 的架構,這一節在來基於圖7講講 編輯SDK 的運行機制。
- 1.通過上一節的介紹,咱們都知道了 WSMediaPlayerView 是整個 編輯SDK 的頂級類。因此咱們由 WSMediaPlayerView 入手,先看圖片最上面。
- 1.能夠看見 WSMediaPlayerView 中會維護一個 30ms的定時循環,這個循環中會不斷的調用 draw frame 來驅動 WSMediaPlayer/NativeWSMediaPlayer 進行視頻/音頻的播放。
- 2.與此同時,最左邊的用戶會經過 play、pause、seek 等 API 來更新 NativeWSMediaPlayer 的狀態。
- 3.須要注意的是,WSMediaPlayerView 的定時循環不會被用戶的 play、pause、seek 等操做所中斷的。
- 2.再來看看圖片左邊,這是 WSMediaPlayer 的內部播放機制。要點爲 三個循環,兩個播放,咱們仍是自底向上解析。
- 1.VideoDecodeService:它內部維護了一個可阻塞循環與一個先進先出隊列——BlockingQueue,當咱們開始播放視頻或者 seek 視頻到某個時間點的時候,VideoDecodeService 會記錄這個開始的時間點,而後不斷的解碼當前時間點以後的每一幀,每解碼出一幀便把這一幀放入 BlockingQueue 中。當隊列中的元素達到最大值時,當前的循環就會被阻塞,直到外部將 BlockingQueue 中的 Top 幀消費了,那麼循環又會被啓動繼續解碼。須要注意的是:VideoDecodeService 只在視頻播放的時候提供視頻幀,由於在這個狀況下 BlockingQueue 中的視頻幀的順序就是視頻真正播放的順序。
- 2.VideoFramePool:它內部維護了一個可阻塞請求循環與一個LruCachePool。通常狀況下 VideoFramePool 的循環是處於阻塞狀態的。當外部 seek 視頻的時候,循環會接收到一個請求並開始處理這個請求,若是 LruCachePool 中有 Cache 被命中了,那麼就直接返回 Cache,不然將會當即從視頻中解碼出這個請求中時間點的視頻幀存到 LruCachePool 中而後再返回。須要注意的是:VideoFramePool 只在視頻 seek 的時候提供視頻幀,由於咱們的 seek 操做是隨機的,因此在這個狀況下 VideoDecodeService 沒法使用。
- 3.AudioDecodeService:它與 VideoDecodeService 相似,也維護了一個可阻塞循環與先進先出隊列,內部的其餘行爲也相似,只是將視頻幀換成了音頻幀。
- 4.FrameRenderer:
- 1.當視頻 seek 的時候,其會從 VideoFramePool 中取出 seek 時刻的視頻幀繪製它。
- 2.當視頻處於 playing 狀態時,它的 drawFrame 方法就會不斷被 WSMediaPlayerView 經過定時循環調用並從 VideoDecodeService 中取出當前幀經過 Open GL 繪製它。
- 5.AudioPlayer:當視頻處於 playing 狀態時,它也會不斷被 WSMediaPlayerView 經過定時循環驅動着從 AudioDecodeService 中取出當前的音頻幀,而後經過反向代理將音頻幀交給 Java 層的 AudioPlayer 進行播放。
4、VideoDecodeService解析
上一章大概的講了講整個 編輯SDK 的總體架構和運行機制,但其實整個 編輯SDK 內部的每個部分的細節都很是多,因此這一章我會先講解 VideoDecodeService 的內部細節。其餘各個部分則放在後面幾篇文章中講解。與此同時,WsVideoEditor 中的代碼也會隨着講解的進行而不斷更新。最終造成一個可用的 編輯SDK。微信
1.API講解
-----代碼塊1----- VideoDecodeService.java private native long newNative(int bufferCapacity);
private native void releaseNative(long nativeAddress);
private native void setProjectNative(long nativeAddress, double startTime, byte[] projectData);
private native void startNative(long nativeAddress);
private native String getRenderFrameNative(long nativeAddress, double renderTime);
private native void updateProjectNative(long nativeAddress, byte[] projectData);
private native void seekNative(long nativeAddress, double seekTime);
private native void stopNative(long nativeAddress);
private native boolean endedNative(long nativeAddress);
private native boolean stoppedNative(long nativeAddress);
private native int getBufferedFrameCountNative(long nativeAddress);
複製代碼
如代碼塊1所示,咱們先來說講 VideoDecodeService 的 API
- 1.
newNative
:由前面幾章的講解咱們知道,VideoDecoderService 內部有一個先進先出的阻塞隊列,這個方法的入參 bufferCapacity
就是用於設置這個阻塞隊列的長度。這個方法調用以後 Native 層會建立一個與 Java 層同名的 VideoDecodeService.cpp 對象。而後返回一個 long
表示這個 Cpp 對象的地址。咱們會將其記錄在 Java 層,後續要調用其餘方法時須要經過這個地址找到相應的對象。
- 2.
releaseNative
:由於 Cpp 沒有垃圾回收機制,因此 Cpp 對象都是須要手動釋放的,因此這個方法就是用於釋放 VideoDecodeService.cpp 對象。
- 3.
setProjectNative
:由於 Protobuf 是高效的跨平臺通訊協議,因此 Java 與 Cpp 層的通訊方式使用的就是 Protobuf,咱們能夠看 ws_video_editor_sdk.proto 這個文件,裏面定義的 EditorProject 就是兩端一塊兒使用的數據結構。這個方法的入參 nativeAddress
就是咱們在 1 中獲取到的對象地址。入參 startTime
表示起始的解碼點,單位是秒。入參 projectData
就是 EditorProject 序列化以後的字節流。
- 4.
startNative
:這個方法表示開始解碼。
- 5.
getRenderFrameNative
:這個方法表示獲取 renderTime
這一時刻的幀數據,目前返回到 Java 層的是一個 String
,在 Cpp 層後續咱們主要就是使用這個方法獲取到的幀數據使用 OpenGL 繪製到屏幕上。
- 6.
updateProjectNative
:這個方法和 setProjectNative
相似,用於更新 EditorProject。
- 7.
seekNative
:咱們在看視頻的時候,將進度條拖動到某一時刻的操做被稱爲 seek,在 VideoDecodeService 中的體現就是這個方法,這個方法會將當前的解碼時間點設置爲 seekTime
。
- 8.
stopNative
:這個方法表示暫停解碼。
- 9.
endedNative
:返回一個 boolean
表示視頻的解碼點是否到達了視頻的結尾。
- 10.
stoppedNative
:返回了一個 boolean
表示當前是否暫停了解碼。
- 11.
getBufferedFrameCountNative
:返回一個 int
,表示當前阻塞隊列中有多少個幀,最大不會超過咱們在 1 中設置的 bufferCapacity
。
2.代碼分析
這一小節中,我使用一個完整的例子來分析 VideoDecodeService 的源碼
- 1.例子在 TestActivity 中,咱們運行項目會看見界面上有三個 Button 和兩個 TextView。
- 2.咱們在
initButton
中進行了下面這些操做
- 1.初始化了 ui。
- 2.建立了一個 VideoDecodeService.java 類,內部就是調用咱們上一節說的
newNative
方法。這個方法最終會進入到 video_decode_service.h 中調用 VideoDecodeService.cpp 的構造方法,構造方法則會建立一個 BlockingQueue.cpp 對象 decoded_unit_queue_
,這就是咱們一直說的 先進先出阻塞隊列
- 3.構建了一個 EditorProject.java,裏面傳了一個須要解碼的視頻路徑
/sdcard/test.mp4
- 3.咱們點擊 START 按鈕
- 1.
stringBuilder
和 times
是用來記錄測試數據的就不說了
- 2.這裏而後調用了
setProject
方法,進過一系列調用鏈後會經過 jni 進入到代碼塊3
- 1.將
buffer
反序列化成 EditorProject.cpp 對象。
- 2.
address
強轉 VideoDecodeService.cpp 對象。
- 3.使用
LoadProject
方法解析出一些數據,例如視頻的幀率、寬高等等。有興趣的讀者能夠跟進入看看。
- 4.調用
SetProject
給 VideoDecodeService.cpp 設置 EditorProject.cpp。
- 3.調用
start
最終也是到代碼塊3中,調用 Start
方法。咱們繼續進入 Start
方法中,發現其中是啓動了一個線程而後調用 VideoDecodeService::DecodeThreadMain
,這個方法內部則是一個 while
循環,每當使用 FFMPEG 解碼出一個視頻幀的時候就會將這一幀放到 decoded_unit_queue_
中。當外部沒有消費者時,decoded_unit_queue_
的幀數量將會很快達到閾值(咱們設置的是10),此時這個線程就會被阻塞。直到外部消費後,幀數量減小了,本線程將會繼續開始解碼視頻幀,如此往復。
-----代碼塊3----- com_whensunset_wsvideoeditorsdk_inner_VideoDecoderService.cc JNIEXPORT void JNICALL Java_com_whensunset_wsvideoeditorsdk_inner_VideoDecodeService_setProjectNative (JNIEnv *env, jobject, jlong address, jdouble render_pos, jbyteArray buffer) {
VideoDecodeService *native_decode_service = reinterpret_cast<VideoDecodeService *>(address);
model::EditorProject project;
jbyte *buffer_elements = env->GetByteArrayElements(buffer, 0);
project.ParseFromArray(buffer_elements, env->GetArrayLength(buffer));
env->ReleaseByteArrayElements(buffer, buffer_elements, 0);
LoadProject(&project);
native_decode_service->SetProject(project, render_pos);
}
JNIEXPORT void JNICALL Java_com_whensunset_wsvideoeditorsdk_inner_VideoDecodeService_startNative (JNIEnv *, jobject, jlong address) {
VideoDecodeService *native_decode_service = reinterpret_cast<VideoDecodeService *>(address);
native_decode_service->Start();
}
複製代碼
- 4.繼續看代碼塊2,能夠看見我啓動了一個 Java 層的無限循環線程,每隔 30ms 會 sleep 一下。每次循環我則會調用
getRenderFrame
方法來從 VideoDecodeService 中消費一個視頻幀。而後把幀的信息打印到 TextView 上面。其實這裏的代碼能夠類比爲視頻的播放,VideoDecodeService 不斷地在後臺線程進行解碼按順序將視頻幀放入到隊列中,本線程則不斷的從隊列中取出一幀進行消費,就像視頻幀被渲染到屏幕上同樣。
- 5.最下面還有一個 Java 層的無限循環線程,會不斷的讀取 VideoDecodeService 的其餘信息打印到 TextView 上。
- 6.這一節只是簡單的介紹 VideoDecodeService 的運行思路,其實代碼裏還有不少實現細節,這些細節解析就只能交給讀者了,畢竟我比較懶:-D
5、尾巴
終於從零開發仿寫一個抖音APP這一系列文章又從新開始更新了,今年以來文章的發表間隔長了不少,寫文章的時間也少了不少。可是爲了那麼多支持、關注個人讀者我也不能就這樣放棄更新。立一個 flag,從此每月都要更新一篇文章,但願你們可以多多支持,感謝!!
連載文章
不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、互聯網、程序員、計算機編程。下面是個人微信公衆號:世界上有意思的事,乾貨多多等你來看。