android sdk build-tools_从零开始仿写一个抖音App——视频编辑SDK开发(一)
本文首發(fā)于微信公眾號——世界上有意思的事,搬運轉(zhuǎn)載請注明出處,否則將追究版權(quán)責(zé)任。交流qq群:859640274。
大家好久不見,又有一個多月沒有發(fā)文章了。不知道還有哪些讀者記得我的?從零開始仿寫抖音App?的系列文章,這個系列的文章已經(jīng)很久沒有更新了,最后一篇文章是我開始開發(fā)?視頻編輯SDK?時寫的。當(dāng)時踏入到了一個新的領(lǐng)域里,未知的東西太多了,導(dǎo)致接下來的大半年都沒有更新相關(guān)的文章。?但是別以為我已經(jīng)放棄了,今天對于我來說是一個值得紀(jì)念的日子,2019年10月28日?我終于將?視頻編輯SDK?的最簡版本給完成了,我將這個?視頻編輯SDK?命名為?WSVideoEditor,接下來的一段時間里我計劃更新 4 篇解析該 SDK 的相關(guān)文章,WsVideoEditor:https://github.com/TheGodsThemselves/WsVideoEditor 中的代碼我也會隨著文章同步更新。當(dāng) SDK 解析完畢之后?從零開始仿寫一個抖音App?系列文章將會踏出最關(guān)鍵的一步。公眾號后臺發(fā)送:視頻編輯SDK開發(fā)(一),可以獲得 PDF 文件。
本文分為以下章節(jié),讀者可按需閱讀:
1.項目介紹
2.SDK功能介紹
3.SDK架構(gòu)以及運行機制介紹
4.VideoDecodeService解析
一、項目介紹
本章我將介紹 WsVideoEditor:https://github.com/TheGodsThemselves/WsVideoEditor 項目的基本結(jié)構(gòu)、組織方式以及運行方式。需要大家把項目 clone 下來跟著我一步步來做。
1.基本結(jié)構(gòu)
我們看著圖1,一個個來講:
1.android:顧名思義,這個目錄下是一個 Android 項目,去掉 .gradle、build、.idea 等等 ignore 的文件,我們主要關(guān)注下面這幾個文件夾。
?? ?
1.ffmpeg-cpp:如圖2,這個文件夾中有 FFMPEG 的頭文件與 .so 文件,我們需要將這個庫集成到我們的 SDK 中,我們的?編輯SDK?需要有解碼視頻的能力,解碼分為硬解和軟解,ffmpeg 就是用于軟解的最強開源庫。至于如何得到這些東西,我之前寫過一篇 FFMPE食用指南:https://juejin.im/post/5c0d3c366fb9a049ea38c6b4 有興趣的讀者可以看看。
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 代碼。更詳細(xì)的解析,會在后面幾章。
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,這個目錄里面主要存放與平臺無關(guān)的 Cpp 代碼,因為我們要做的是一個跨平臺的視頻編輯 SDK,所以盡量多的將與平臺無關(guān)的代碼進(jìn)行共用,是一個明智的選擇。可以看見里面的 prebuilt_protobuf 目錄下就有我們使用 build_proto.sh 生成的 Cpp 文件,這些文件就是可以共用的。?
5.sharedproto:這里存放著我們定義的 Protobuf 文件。
6.thirdparty:這里存放著一些包含源碼的與平臺無關(guān)的三方庫,例如 libyuv。
7.CMakeLists.txt:這個文件主要是為了讓 Clion 能夠識別我們這個整個項目。
2.如何運行項目
1.git clone https://github.com/TheGodsThemselves/WsVideoEditor.git
2.NDK 環(huán)境需要準(zhǔn)備好
3.用 Android Studio 打開?WsVideoEditor/android?目錄
4.在手機中準(zhǔn)備?/sdcard/test.mp4?視頻文件
5.運行?wsvideoeditor-test?項目
二、SDK功能介紹
這一章我們來介紹一下?編輯SDK?目前有的以及未來會有的功能。編輯SDK?的最終形態(tài)會和抖音的視頻編輯功能接近,有其他想法的讀者也可以在評論區(qū)留言或者提 issue。
1.目前有的功能
1.開始播放
2.暫停播放
3.視頻音量調(diào)整
4.單段視頻播放
5.多段視頻播放
6.視頻 Seek
7.視頻邊緣模糊填充
2.規(guī)劃中的功能
1.視頻類:
1.按時間軸添加額外的聲音
2.按時間軸添加濾鏡
3.按時間軸添加靜態(tài)貼紙、動態(tài)貼紙
4.多段視頻間轉(zhuǎn)場
2.圖片類:
1.添加聲音
2.多張圖片間的轉(zhuǎn)場
3.照片電影
3.工具類:
1.視頻縮略圖截取
2.視頻元數(shù)據(jù)讀取
4.編碼類:
1.導(dǎo)出不同格式的視頻
2.更改視頻的分辨率、幀率
3.視頻轉(zhuǎn) gif
5.技術(shù)類:
1.多進(jìn)程編解碼視頻
2.多進(jìn)程播放視頻
3.多進(jìn)程視頻縮略圖截取
三、SDK架構(gòu)以及運行機制介紹
這一章我來介紹一下目前?編輯SDK?的整體架構(gòu)以及運行機制。
1.編輯SDK架構(gòu)
圖6是 編輯SDK 的架構(gòu)圖,這一節(jié)我會照著這張圖來介紹。
(1).基礎(chǔ)API
先從底部看起,底部是整個 SDK 依賴的底層 API 庫。
1.FFMPEG:前面簡單介紹過,是一個開源的視頻庫,在我們的項目中主要用于軟編解碼。
2.MediaCodec:是 Android 中的硬編解碼?API,相應(yīng)的 iOS 也有自己的硬編解碼方式。
3.OpenGL:是一個開源的圖形庫,Android 和 iOS 中都有內(nèi)置 OpenGL ES 作為默認(rèn)圖形庫。在我們的項目中主要用于將視頻解碼后的視頻幀繪制到屏幕上去。當(dāng)然也可以對這些圖像做一些效果的變化,例如濾鏡、視頻/圖片轉(zhuǎn)場等等。
4.Libyuv:是 Google 開源的實現(xiàn)各種 YUV 與 RGB 之間相互轉(zhuǎn)換、旋轉(zhuǎn)、縮放的庫。
5.Protobuf:是 Google 開源的一種平臺無關(guān)、語言無關(guān)、可擴展且輕便高效的序列化數(shù)據(jù)結(jié)構(gòu)的協(xié)議。在我們的項目中主要用于 Cpp 與 Java、OC、Dart 之間的數(shù)據(jù)通信。
(2).SDK主體
接著我們再看圖片中的主體部分,因為目前只有 Android 端的實現(xiàn),所以主體部分的上層實現(xiàn)我使用 Android 來代替。
1.Android層架構(gòu):
1.WSMediaPlayerView:繼承于 TextureView,所以其可以提供一個具有 Open GL 環(huán)境的線程。對 Surface 家族不了解的同學(xué)可以看看這兩篇文章:Android繪制機制以及Surface家族源碼全解析:https://juejin.im/post/5c666a9ff265da2d8b632bf9、相機/OpenGL/視頻/Flutter和SurfaceView:https://juejin.im/post/5d9fd2415188256bf067e23d#heading-5)
2.WSMediaPlayer:這個是一個代理了 Native 的 NativeWSMediaPlayer 的 Java 類。該類具有一個播放器應(yīng)該有的各種 API,例如 play、pause、seek 等等。其實很多 Android 中的系統(tǒng)類都是以這種形式存在的,例如 Bitmap、Surface、Canvas 等等。說到底 Java 只是 Android 系統(tǒng)方便開發(fā)者開發(fā) App 的上層語言,系統(tǒng)中大部分的功能最終都會走到 Native 中去,所以讀者需要習(xí)慣這種代碼邏輯
3.AudioPlayer:這個類是基于 Android 中的 AudioTrack 封裝的能夠播放音頻幀的 Java 類。我們在 Native 層也有一個 AudioPlayer。這里與 WSMediaPlayer 相反 Native 層的 AudioPlayer 是一個空殼,Java 層的 AudioPlayer 反向代理了 Native 層的 AudioPlayer,因為在這里 Java 層的 AudioPlayer 才是真正播放音頻的東西。
2.Native層架構(gòu):這里我們自底向上來剖析,Native 層的架構(gòu)
1.AudioDecodeService:它負(fù)責(zé)使用 FFMPEG/MediaCodec,來從視頻/音頻中解碼出某個時間點的音頻幀,并且存儲在一個音頻幀隊列中。最終被外部取出音頻幀交給音頻播放器播放。
2.VideoDecodeService:它和 AudioDecodeService 類似,是使用 FFMPEG/MediaCodec 來從視頻中解碼出某個時間點的視頻幀并且存儲在一個視頻幀隊列中。最終被外部取出視頻幀交給 OpenGL 繪制到屏幕上。
3.VideoFramePool:它負(fù)責(zé)響應(yīng)外部的 seek 事件,然后使用 FFMPEG/MediaCodec 來從視頻中解碼出當(dāng)前時間點的視頻幀,然后存儲到一個 LruCache 中同時返回 seek 時間點的視頻幀。
4.AudioPlayer:前面說過,這個是 Java 層的 AudioPlayer 代理類,主要用于播放 AudioDecodeService 解碼出來音頻幀。
5.FrameRenderer:這個東西是一個渲染器,在視頻播放時用于渲染 VideoDecodeService 不斷解碼出的視頻幀,在視頻 seek 的時用于向 VideoDecoderPool 發(fā)送 seek 請求,然后渲染返回的視頻幀。
6.NativeWSMediaPlayer:用于同步 AudioPlayer 和 FrameRenderer 的音視頻播放。即我們一般認(rèn)為的視頻播放器實體,被 Java 層的 WSMediaPlayer 代理著。
2.編輯SDK運行機制
上一節(jié)講解了 編輯SDK 的架構(gòu),這一節(jié)在來基于圖7講講 編輯SDK 的運行機制。
1.經(jīng)過上一節(jié)的介紹,我們都知道了 WSMediaPlayerView 是整個 編輯SDK 的頂級類。所以我們由 WSMediaPlayerView 入手,先看圖片最上面。
1.可以看見 WSMediaPlayerView 中會維護(hù)一個?30ms的定時循環(huán),這個循環(huán)中會不斷的調(diào)用 draw frame 來驅(qū)動 WSMediaPlayer/NativeWSMediaPlayer 進(jìn)行視頻/音頻的播放。
2.與此同時,最左邊的用戶會通過 play、pause、seek 等 API 來更新 NativeWSMediaPlayer 的狀態(tài)。
3.需要注意的是,WSMediaPlayerView 的定時循環(huán)不會被用戶的 play、pause、seek 等操作所中斷的。
2.再來看看圖片左邊,這是 WSMediaPlayer 的內(nèi)部播放機制。要點為?三個循環(huán),兩個播放,我們還是自底向上解析。
1.VideoDecodeService:它內(nèi)部維護(hù)了一個可阻塞循環(huán)與一個先進(jìn)先出隊列——BlockingQueue,當(dāng)我們開始播放視頻或者 seek 視頻到某個時間點的時候,VideoDecodeService 會記錄這個開始的時間點,然后不斷的解碼當(dāng)前時間點之后的每一幀,每解碼出一幀便把這一幀放入 BlockingQueue 中。當(dāng)隊列中的元素達(dá)到最大值時,當(dāng)前的循環(huán)就會被阻塞,直到外部將 BlockingQueue 中的 Top 幀消費了,那么循環(huán)又會被啟動繼續(xù)解碼。需要注意的是:VideoDecodeService 只在視頻播放的時候提供視頻幀,因為在這個情況下 BlockingQueue 中的視頻幀的順序就是視頻真正播放的順序。
2.VideoFramePool:它內(nèi)部維護(hù)了一個可阻塞請求循環(huán)與一個LruCachePool。一般情況下 VideoFramePool 的循環(huán)是處于阻塞狀態(tài)的。當(dāng)外部 seek 視頻的時候,循環(huán)會接收到一個請求并開始處理這個請求,如果 LruCachePool 中有 Cache 被命中了,那么就直接返回 Cache,否則將會立即從視頻中解碼出這個請求中時間點的視頻幀存到 LruCachePool 中然后再返回。需要注意的是:VideoFramePool 只在視頻 seek 的時候提供視頻幀,因為我們的 seek 操作是隨機的,所以在這個情況下 VideoDecodeService 無法使用。
3.AudioDecodeService:它與 VideoDecodeService 類似,也維護(hù)了一個可阻塞循環(huán)與先進(jìn)先出隊列,內(nèi)部的其他行為也類似,只是將視頻幀換成了音頻幀。
4.FrameRenderer:
1.當(dāng)視頻 seek 的時候,其會從 VideoFramePool 中取出 seek 時刻的視頻幀繪制它。
2.當(dāng)視頻處于 playing 狀態(tài)時,它的 drawFrame 方法就會不斷被 WSMediaPlayerView 通過定時循環(huán)調(diào)用并從 VideoDecodeService 中取出當(dāng)前幀通過 Open GL 繪制它。
5.AudioPlayer:當(dāng)視頻處于 playing 狀態(tài)時,它也會不斷被 WSMediaPlayerView 通過定時循環(huán)驅(qū)動著從 AudioDecodeService 中取出當(dāng)前的音頻幀,然后通過反向代理將音頻幀交給 Java 層的 AudioPlayer 進(jìn)行播放。
四、VideoDecodeService解析
上一章大概的講了講整個?編輯SDK?的整體架構(gòu)和運行機制,但其實整個?編輯SDK?內(nèi)部的每一個部分的細(xì)節(jié)都非常多,所以這一章我會先講解 VideoDecodeService 的內(nèi)部細(xì)節(jié)。其他各個部分則放在后面幾篇文章中講解。與此同時,WsVideoEditor https://github.com/TheGodsThemselves/WsVideoEditor 中的代碼也會隨著講解的進(jìn)行而不斷更新。最終形成一個可用的?編輯SDK。
1.API講解
-----代碼塊1----- VideoDecodeService.javaprivate 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 內(nèi)部有一個先進(jìn)先出的阻塞隊列,這個方法的入?yún)?bufferCapacity?就是用于設(shè)置這個阻塞隊列的長度。這個方法調(diào)用之后 Native 層會創(chuàng)建一個與 Java 層同名的 VideoDecodeService.cpp 對象。然后返回一個?long?表示這個 Cpp 對象的地址。我們會將其記錄在 Java 層,后續(xù)要調(diào)用其他方法時需要通過這個地址找到相應(yīng)的對象。
2.releaseNative:因為 Cpp 沒有垃圾回收機制,所以 Cpp 對象都是需要手動釋放的,所以這個方法就是用于釋放 VideoDecodeService.cpp 對象。
3.setProjectNative:因為 Protobuf 是高效的跨平臺通信協(xié)議,所以 Java 與 Cpp 層的通信方式使用的就是 Protobuf,我們可以看?ws_video_editor_sdk.proto?這個文件,里面定義的 EditorProject 就是兩端一起使用的數(shù)據(jù)結(jié)構(gòu)。這個方法的入?yún)?nativeAddress?就是我們在 1 中獲取到的對象地址。入?yún)?startTime?表示起始的解碼點,單位是秒。入?yún)?projectData?就是 EditorProject 序列化之后的字節(jié)流。
4.startNative:這個方法表示開始解碼。
5.getRenderFrameNative:這個方法表示獲取?renderTime?這一時刻的幀數(shù)據(jù),目前返回到 Java 層的是一個?String,在 Cpp 層后續(xù)我們主要就是使用這個方法獲取到的幀數(shù)據(jù)使用 OpenGL 繪制到屏幕上。?
6.updateProjectNative:這個方法和?setProjectNative?類似,用于更新 EditorProject。
7.seekNative:我們在看視頻的時候,將進(jìn)度條拖動到某一時刻的操作被稱為 seek,在 VideoDecodeService 中的體現(xiàn)就是這個方法,這個方法會將當(dāng)前的解碼時間點設(shè)置為?seekTime。
8.stopNative:這個方法表示暫停解碼。
9.endedNative:返回一個?boolean?表示視頻的解碼點是否到達(dá)了視頻的結(jié)尾。
10.stoppedNative:返回了一個?boolean?表示當(dāng)前是否暫停了解碼。
11.getBufferedFrameCountNative:返回一個?int,表示當(dāng)前阻塞隊列中有多少個幀,最大不會超過我們在 1 中設(shè)置的?bufferCapacity。?
2.代碼分析
這一小節(jié)中,我使用一個完整的例子來分析 VideoDecodeService 的源碼
1.例子在 TestActivity 中,我們運行項目會看見界面上有三個 Button 和兩個 TextView。
2.我們在?initButton?中進(jìn)行了下面這些操作
1.初始化了 ui。
2.創(chuàng)建了一個 VideoDecodeService.java 類,內(nèi)部就是調(diào)用我們上一節(jié)說的?newNative方法。這個方法最終會進(jìn)入到?video_decode_service.h?中調(diào)用 VideoDecodeService.cpp 的構(gòu)造方法,構(gòu)造方法則會創(chuàng)建一個 BlockingQueue.cpp 對象?decoded_unit_queue_,這就是我們一直說的?先進(jìn)先出阻塞隊列
3.構(gòu)建了一個 EditorProject.java,里面?zhèn)髁艘粋€需要解碼的視頻路徑?/sdcard/test.mp4
3.我們點擊 START 按鈕
1.stringBuilder?和?times?是用來記錄測試數(shù)據(jù)的就不說了
2.這里然后調(diào)用了?setProject?方法,經(jīng)過一系列調(diào)用鏈后會通過 jni 進(jìn)入到代碼塊3
1.將?buffer?反序列化成 EditorProject.cpp 對象。
2.address?強轉(zhuǎn) VideoDecodeService.cpp 對象。
3.使用?LoadProject?方法解析出一些數(shù)據(jù),例如視頻的幀率、寬高等等。有興趣的讀者可以跟進(jìn)入看看。
4.調(diào)用?SetProject?給 VideoDecodeService.cpp 設(shè)置 EditorProject.cpp。
3.調(diào)用?start?最終也是到代碼塊3中,調(diào)用?Start?方法。我們繼續(xù)進(jìn)入?Start?方法中,發(fā)現(xiàn)其中是啟動了一個線程然后調(diào)用?VideoDecodeService::DecodeThreadMain,這個方法內(nèi)部則是一個?while?循環(huán),每當(dāng)使用 FFMPEG 解碼出一個視頻幀的時候就會將這一幀放到?decoded_unit_queue_?中。當(dāng)外部沒有消費者時,decoded_unit_queue_?的幀數(shù)量將會很快達(dá)到閾值(我們設(shè)置的是10),此時這個線程就會被阻塞。直到外部消費后,幀數(shù)量減少了,本線程將會繼續(xù)開始解碼視頻幀,如此往復(fù)。
4.繼續(xù)看代碼塊2,可以看見我啟動了一個 Java 層的無限循環(huán)線程,每隔 30ms 會 sleep 一下。每次循環(huán)我則會調(diào)用?getRenderFrame?方法來從 VideoDecodeService 中消費一個視頻幀。然后把幀的信息打印到 TextView 上面。其實這里的代碼可以類比為視頻的播放,VideoDecodeService 不斷地在后臺線程進(jìn)行解碼按順序?qū)⒁曨l幀放入到隊列中,本線程則不斷的從隊列中取出一幀進(jìn)行消費,就像視頻幀被渲染到屏幕上一樣。
5.最下面還有一個 Java 層的無限循環(huán)線程,會不斷的讀取 VideoDecodeService 的其他信息打印到 TextView 上。
6.這一節(jié)只是簡單的介紹 VideoDecodeService 的運行思路,其實代碼里還有很多實現(xiàn)細(xì)節(jié),這些細(xì)節(jié)解析就只能交給讀者了,畢竟我比較懶:-D
五、尾巴
終于從零開發(fā)仿寫一個抖音APP這一系列文章又重新開始更新了,今年以來文章的發(fā)表間隔長了很多,寫文章的時間也少了很多。但是為了那么多支持、關(guān)注我的讀者我也不能就這樣放棄更新。立一個 flag,今后每個月都要更新一篇文章,希望大家能夠多多支持,感謝!!
不販賣焦慮,也不標(biāo)題黨。分享一些這個世界上有意思的事情。題材包括且不限于:科幻、科學(xué)、科技、互聯(lián)網(wǎng)、程序員、計算機編程。下面是我的微信公眾號:世界上有意思的事,干貨多多等你來看。
總結(jié)
以上是生活随笔為你收集整理的android sdk build-tools_从零开始仿写一个抖音App——视频编辑SDK开发(一)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓qq登录页面代码(安卓qq登录)
- 下一篇: python处理csv文件列错位_CSV