一、背景介紹
随着4G網絡的推廣和網絡帶寬的提升,視訊成為網際網路使用者主要的消費載體,使用者通過短視訊來分享和浏覽資訊。由此視訊的編輯功能越來越重要、越來越普遍。視訊編輯的App也如雨後春筍般湧現。
為更好地推動得物App社群業務的發展,得物也自研符合得物需求的視訊編輯工具。我們緻力于打造一個“更快、更強”的視訊編輯工具。
二、視訊編輯工具介紹
為了讓大家更好地了解得物App的視訊編輯工具,我們先簡單介紹一下視訊編輯工具的主要功能。
下面是得物App視訊編輯工具的主要功能:
視訊編輯工具的重點如下:
- 視訊編輯工具需要操作的資源:
- 文字:包括普通的文字、特殊的藝術字、花字等等;
- 圖檔:包括靜态圖,如JPEG/PNG等等,也包括HEIC/GIF等動态圖;
- 視訊:包括各種各樣的視訊(各種編碼和封裝格式),主流的格式一般是MP4的封裝格式、H264視訊編碼格式、AAC音頻編碼格式等等;
- 音頻:包括各種各樣的音頻(各種編碼和封裝格式),當然視訊當然也是包含音頻軌道的。
- 視訊編輯工具主要的操作方式:
- 操作圖檔、視訊幀:我們知道視訊是一幀一幀的圖檔組成的,是以操作視訊幀和操作圖檔是一樣的道理,我們通過添加一些特效在圖檔和視訊幀上面,實作一些有趣的效果來吸引使用者。
- 操作音頻:主流的操作音頻方式如倍速、調整音量、變調等等,都是現今短視訊的主要玩法。
- 視訊編輯工具最終生成的是一個新的視訊,這個視訊将特定的資源應用一些特效生成一個新的視訊。
下面的流程圖可以很友善地讓大家了解視訊編輯的工作流程。為了友善,我們輸入一個視訊,加上一些特效,生成一個新的視訊。
從上面的流程可以看出來,原始視訊A.mp4經過解封裝分離出音頻軌道和視訊軌道,對它們解碼之後,對音頻資料應用音頻特效、對視訊幀資料應用視訊特效,然後編碼封裝合成一個新的視訊。當然解碼和編碼都是有一個隊列控制的,流程圖上标注了,沒有深入展開,大家了解即可。
經過上面的介紹,大家對視訊編輯工具有了大概得了解,其實衡量一個視訊編輯工具做得好不好,主要從下面這幾個方面着手:
- 記憶體占用情況
- 導出視訊的速度如何
- 導出視訊的清晰度如何
下面從這三方面詳細展開給大家闡述得物App的視訊編輯工具優化的心路曆程。
三、記憶體優化
性能是所有程式好不好的首要名額,一個工具即使功能再強大,但是一點就崩潰,或者用着用着記憶體暴漲、應用卡死,估計這個應用不能稱為一個優秀的應用,下面我們具體談一談視訊編輯工具的優化檢測方案。
優化記憶體從良好的編碼習慣開始,尤其對音視訊這種對記憶體需求非常高的應用而言。例如一個1080 * 1920的視訊,解碼出來原始資料一幀圖檔大小也是1080 * 1920,占用記憶體是1080 * 1920 * (8 * 3 ) / 8 = 5.93 MB,一個視訊幀就占用這麼大,1秒一般有30幀,那得占用177.9MB,如果不加控制,那不管多高性能的手機也經不住這樣的折騰。希望下面的記憶體檢測和優化方案可以給你帶來一些幫助。
3.1 合理設計隊列
上面我們在介紹視訊編輯流程的視訊談到了解碼隊列和編碼隊列的概念。其實隊列這個概念在音視訊中使用非常頻繁,正是因為記憶體的限制,是以才引入隊列這個控制方式。大家可能還有點懵,但是看完下面的流程圖,我相信你一定會豁然開朗。
我們僅選取解碼的部分來分析一下隊列的重要應用。
在視訊編輯工具中有幾個重要的隊列:
- 解碼過程中:
- Video Packet Queue:視訊解碼之前Packet存放的隊列,一般建議的隊列大小是100
- Audio Packet Queue:音頻解碼之前Packet存放的隊列,一般建議的隊列大小是150
- Video Frame Queue:視訊解碼之後Frame存放的隊列,一般建議的隊列大小是3
- Audio Frame Queue:音頻解碼之後Frame存放的隊列,一般建議的隊列大小是8
- 編碼過程中:
- Encode Video Packet Queue:視訊編碼之後Packet存放的隊列,一般建議的大小是100
- Encode Audio Packet Queue:音頻編碼之後的Packet存放的隊列,一般建議的大小是150
按照上面的方式設計隊列的大小,可以在保證功能正常的情況下最大程度地降低記憶體占用,提升使用者體驗。
3.2 排查記憶體洩漏
Android上排查記憶體洩漏的方式有很多,這裡介紹兩種:
- Asan檢測
- Profile檢測
Asan全稱是AddressSanitizer是一種基于編譯器的快速檢測的工具,用于檢測原生代碼中的記憶體錯誤問題,Asan可以解決如下四種核心問題:
- 堆棧和堆緩沖區上溢、下溢
- 釋放之後堆重新使用問題
- 超過範圍的堆棧使用情況
- 重複釋放、錯誤釋放問題
Asan的使用方式建議參考google官方文檔,這兒就不多作介紹了:https://github.com/google/sanitizers/wiki/AddressSanitizer
關于Profile的使用,如果需要檢測Native記憶體使用情況,需要滿足API>=29,大家在使用的時候需要非常注意。
下面是我們在demo中應用Asan抓取的堆棧:
20042-20042/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
20042-20042/? A/DEBUG: Build fingerprint: 'samsung/t2qzcx/t2q:11/RP1A.200720.012/G9960ZCU2AUGE:user/release-keys'
20042-20042/? A/DEBUG: Revision: '13'
20042-20042/? A/DEBUG: ABI: 'arm64'
20042-20042/? A/DEBUG: Timestamp: 2021-09-17 00:32:31+0800
20042-20042/? A/DEBUG: pid: 19946, tid: 20011, name: AudioTrack >>> com.jeffmony.audioplayer <<<
20042-20042/? A/DEBUG: uid: 10350
20042-20042/? A/DEBUG: signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
2021-09-17 00:32:31.157 20042-20042/? A/DEBUG: Abort message: '=================================================================
==19946==ERROR: AddressSanitizer: heap-use-after-free on address 0x004ac1e41080 at pc 0x007157f69580 bp 0x00705c0bb350 sp 0x00705c0bab08
READ of size 1792 at 0x004ac1e41080 thread T32 (AudioTrack)
#0 0x7157f6957c (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libclang_rt.asan-aarch64-android.so+0x9f57c)
#1 0x706549c228 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x14228)
#2 0x706549bcd4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x13cd4)
#3 0x70654994f0 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x114f0)
#4 0x70654a9cbc (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x21cbc)
#5 0x70654a91d4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x211d4)
#6 0x715af9d188 (/system/lib64/libwilhelm.so+0x1c188)
#7 0x71570ea290 (/system/lib64/libaudioclient.so+0x8b290)
#8 0x71570e9480 (/system/lib64/libaudioclient.so+0x8a480)
#9 0x7156b664d4 (/system/lib64/libutils.so+0x154d4)
#10 0x71593e9974 (/system/lib64/libandroid_runtime.so+0xa5974)
#11 0x7156b65db0 (/system/lib64/libutils.so+0x14db0)
#12 0x7156ace234 (/apex/com.android.runtime/lib64/bionic/libc.so+0xb6234)
#13 0x7156a68e64 (/apex/com.android.runtime/lib64/bionic/libc.so+0x50e64)
0x004ac1e41080 is located 0 bytes inside of 1792-byte region [0x004ac1e41080,0x004ac1e41780) freed by thread T32 (AudioTrack) here: #0 0x7157f74c64 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libclang_rt.asan-aarch64-android.so+0xaac64) #1 0x70654a6d2c (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x1ed2c) #2 0x70654a6af0 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x1eaf0) #3 0x706549bf4c (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x13f4c) #4 0x706549bcd4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x13cd4) #5 0x70654994f0 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x114f0) #6 0x70654a9cbc (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x21cbc) #7 0x70654a91d4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so+0x211d4) #8 0x715af9d188 (/system/lib64/libwilhelm.so+0x1c188) #9 0x71570ea290 (/system/lib64/libaudioclient.so+0x8b290)
顯示message是:heap-use-after-free on address 0x004ac1e41080 說明是使用了已經釋放掉的記憶體了,再繼續看,這個記憶體具體在什麼地方被釋放的?0x004ac1e41080 is located 0 bytes inside of 1792-byte region [0x004ac1e41080,0x004ac1e41780) Asan一個很大的優勢就是可以追蹤記憶體釋放的路徑,防止出現記憶體洩漏和野指針問題,特别是野指針,一旦出現特别難排查,簡直是C++開發的噩夢,希望大家用好工具,同時培養良好的C++編碼習慣。
3.3 優化線程
另一個影響記憶體的重要因素是線程,視訊編輯工具涉及到的線程非常多,線程的使用得遵循一些基本的原則:
- 盡量少建立線程
- 盡量少使用pthread_mutex_t
- 本着功能隔絕原則使用線程
- 能同步就别異步
以編輯子產品為例,這兒列一下我們使用到的所有線程:
- GL處理線程
- 視訊解封裝線程
- 視訊中視訊軌道解碼線程
- 視訊音頻軌道解碼線程
- 抽取縮略圖線程
- 音頻編碼線程
- 視訊編碼線程
- 視訊封裝線程
如果插入了獨立的音頻檔案,還需要添加兩個額外的線程:
- 音樂檔案播放線程
- 音樂檔案解碼線程
上面列出的是一個視訊編輯工具能正常工作所必備的最少線程,如果你的視訊編輯工具中多了什麼線程,我們建議可以适當優化一下,畢竟少一個線程,可以少一分開銷,而且少一分線程同步的工作。
我們在底層也按照Android的消息機制重寫了一套C++層的消息分發SDK,這個我們後續會另外分享文章闡釋我們定制的消息分發SDK,這兒點到為止。
四、提升導出視訊的速度
我們使用視訊編輯工具,最終是希望導出一個視訊,如果這個導出的過程很慢,那肯定是無法忍受的,從上面的介紹我們已知視訊的導出需要經過“解碼——應用特效——編碼”的過程,其中解碼和編碼這兩個過程對速度的影響至關重要。因為解碼和編碼視訊需要耗費大量的資源,目前主要有兩種方式——“軟解/編碼”和“硬解/編碼”。
如果你使用過FFmpeg或者其他使用CPU進行視訊編解碼的來處理視訊的話,你可能已經遇到了處理速度慢的問題。這主要是因為軟編碼和軟解碼使用CPU進行運算,而CPU在處理視訊上的速度遠低于DSP晶片;簡而言之“軟解/編碼”主要通過CPU來工作,通過CPU來主導大量的計算工作,是原始的處理方式,當然耗費的時間也比較長;“硬解/編碼”是通過GPU來處理,GPU是專用的圖形處理晶片,對視訊的解碼和編碼有專門的優化,是以編碼和解碼的速度非常快。
Android上使用MediaCodec來實作“硬解/編碼”,iOS上使用VideoToolBox來實作“硬解/編碼”,這裡着重介紹Android上編碼解碼的速度優化。
從上面的流程我們可以看出,編碼在解碼的後面,一個時長60s(30fps)的視訊,需要解碼1800幀,然後編碼1800幀視訊才能完整生成另外一個視訊,這樣串行的等待是耗時的主要原因。
這時候我們參考多線程方案,将一個60s的視訊均分為兩段,然後這兩段視訊同時進行解碼操作,生成導出了兩個30s的臨時緩存視訊檔案,随後将這兩個30s的視訊合并為一個60s的B.mp4視訊,最後删除臨時緩存檔案,這樣我們隻需要同時處理900幀的資料,理論上可以提升一倍的導出速度。
這就是并行導出,下面是得物App并行導出的基本流程。
首先我們要明确導出視訊是需要消耗資源的,這個資源就是MediaCodec,最終是送入到GPU中處理,一個手機中的MediaCodec執行個體是有限的,正常情況下,一個手機可以提供的MediaCodec執行個體最多有16個,如果目前使用的MediaCodec執行個體超過16個,那麼手機将無法正常工作。MediaCodec資源是手機中的所有App共同持有。是以并行分段的個數不是越多越好。
- 隻有一段,需要兩個MediaCodec(一個用來解碼視訊,一個用來編碼視訊),注意:音頻的解碼和編碼可以不要用MediaCodec,畢竟音頻的耗時少多了,不是瓶頸。
- 分成兩段需要四個MediaCodec,分成三段需要六個MediaCodec,分成四段需要八個MediaCodec,以此類推。
下面是并行導出的測試結果:
兩段并行速度提升50% ~ 70%,記憶體增加20%, 三段并行速度提升60% ~ 90%,記憶體增加80%;并行超過三段的話就無法明顯提升速度了。我們比較建議并行兩段,在一些性能很好的機型上并行三段。
如果有些同學對視訊導出過程中檔案操作還有疑問的,下面的示意圖可以比較清楚地看出并行導出操作本地檔案的過程:
- 并行導出的過程中,生成了兩個臨時檔案
- 并行導出完成後,這兩個臨時檔案合并為一個新的檔案,兩個臨時生成的檔案被删除了(節省使用者寶貴的存儲空間)
- 原始檔案jeffmony_out.mp4并沒有被删除/修改
Tips:目前我們在處理過程中生成的臨時檔案和最終的适配檔案都會儲存在/sdcard/Pictures/duapp/Compile/下,而在處理完成後的臨時檔案清理過程會觸發在某些機型上的保護機制,建議後續調整到App的私有目錄下。
當然還有其他的提升導出速度的建議,例如在視訊幀特效處理的過程中,我們建議:
- 盡量采用FBO/EBO/ABO方式處理texture
- 紋理如果過大要進行壓縮
- 嚴禁采用glFinish()
這些做法都是我們在視訊編輯開發過程中的切實經驗,希望能給大家帶來一些幫助。
五、提升導出視訊的清晰度
一個視訊編輯功能是否足夠優秀,其中的一個重要名額就是同等條件下導出的視訊是否足夠清楚,通常而言,衡量視訊是否清晰的有兩種方式:
- 主觀标準:找一些使用者觀看不同的視訊,根據使用者的觀感輸出視訊清晰度的對比結果,使用者一般根據色彩、畫面亮度、柔和度等來評估清晰度。
- 客觀标準:利用算法計算視訊畫面品質分,目前比較推薦Netflix推出的開源庫VMAF來計算視訊幀的品質分。
實際上主觀标準是比較準确的,但是可操作性比較差,特别是處理海量視訊的時候,需要大量的人力,無法有效開展,是以日常工作中還是推薦客觀标準進行海量計算,主觀标準進行重點判斷。具體的可以結合業務的重要程度來開展。
下面結合我們實際的工作給出具體提升視訊清晰度的方式:
視訊基礎編碼資訊優化
- Profile優化:Profile有三種Level,分别是Baseline、Main、High,其中Baseline Profile對應清晰度最低,Android 3.0之後的版本都支援的,Main Profile清晰度比Baseline Profile清晰度要好,但是從Android 7.0之後才支援,High Profile清晰度最高,也是從Android 7.0之後才支援。我們在設定Encoder Profile Level之前,需要判斷一下目前是否支援。
- Bitrate 碼率 設定: 視訊碼率是視訊資料傳輸時機關時間内傳送的資料位數。機關是kbps,望文生義,碼率越大,機關時間填充的資料就越多,視訊品質就越高。但碼率也不是設定的越大越好,超過必要限度,對視訊畫質的提升已不明顯,建議采用合适的factor來調整碼率。Bitrate = width * height * frameRate * factor,其中factor=0.15。
- Bitrate Mode: 有三種通過的編碼模式——VBR(可變碼率)、CBR(固定碼率)、ABR(平均碼率),其中ABR是最好的方式,可以兼顧品質和視訊大小。
- B幀設定: 視訊有I幀、P幀、B幀構成,其中I幀最大,P幀次之,B幀最小,我們在編碼時盡量多設定B幀(在合理的範圍内),并不會降低清晰度,但是可以大大降低視訊的大小,這樣我們就可以相應地調大碼率,最終實作了提升清晰度的目标。
HEVC 編碼優化: 使用HEVC編碼,可以保證在不增加檔案大小的情況下,大大提升視訊的清晰度。在相同的圖像品質下,HEVC編碼的視訊比H.264編碼的視訊約減少40%
色彩調優
- 綜合調整亮度、對比度、色溫、飽和度、銳度等顔色參數,進而優化整體的視訊畫面,讓視訊畫面看上去“更清晰”。
超分算法 : 采用ESRGAN算法,利用機器學習的優勢對圖檔和視訊進行去模糊、Resize、降噪、銳化等處理,重建圖檔,實作對圖檔的超分辨率處理。
- 特征提取:計算噪點
- 非線性映射:放大,模糊化噪點
- 圖像重建:差分,平滑過度,去噪
下面是使用超分算法處理前後的對比圖,可以很明顯地看出右邊的圖更加清晰,少了很多噪點、圖檔更亮、過度更平滑。
如果大家想了解視訊清晰度優化的技術細節,可以參考文章--視訊清晰度優化指南
六、總結
本文開篇從介紹得物App的主要功能展開,提出了視訊編輯工具優化的三個次元:
- 優化記憶體占用
- 提升視訊導出速度
- 提升導出視訊的清晰度
其中在“提升視訊導出速度”時重點談到了“并行導出”的技術方案,從最終的結果來看,視訊導出速度的提升非常明顯,同時也非常清楚地解釋了“并行導出”過程中為什麼生成臨時檔案?為什麼有必要在導出完成之後删除臨時檔案?盡力給使用者帶來較好的體驗。
最後在“提升導出視訊的清晰度”中重點提到的超分算法應用效果提升明顯,超分之後的視訊幀相比原幀圖更加清晰、噪點更少,而且細節部分更加真實。
後續我們還會結合AR特效輸出更多有意義的技術分享,敬請期待。
文 /Jeff Mony
關注得物技術,每周一三五晚18:30更新技術幹貨
要是覺得文章對你有幫助的話,歡迎評論轉發點贊~