移動端音影片需求實現方案探索

語言: CN / TW / HK

一起養成寫作習慣!這是我參與「掘金日新計劃 · 4 月更文挑戰」的第1天,點選檢視活動詳情

通過這篇文章,你將收穫:
1. 音影片的相關知識點和技術
2. Flutter中關於音影片操作的相關技術
3. 商用場景下,音影片需求的主流實現方案

寫作背景

最近的專案是一個健身類的Flutter App,其中核心功能是訓練課程的播放。由於多種因素導致使用者體驗非常差,因此在接手專案的半年裡我對這塊功能做了2次改造,並且預研了一套長期的方案以支援課程播放這個核心功能的持續迭代。所以記錄分享給大家,避免重複踩坑走彎路。
需要說明的是:音影片(或者說任何技術難題)的難點都是在於解決方案,而不是任何具體的實現程式碼;而方案往往不是一蹴而就,因為涉及到前端需求、開發資源、團隊資源等一大堆問題,接下來我會著重聊聊我在音影片需求實現方案的整個探索過程

落實方案的坎坷之路

一、亡羊補牢

專案初期,App的課程直接線上播放,也不做快取機制,使用者訓練過程中進行出現訓練5秒等待10秒的情況,而且每次都線上載入,極其耗費流量。因此我們對影片進行分析得到位元速率竟然達到了16000kbps+,以至於短短30秒的影片就有60MB,這是播放過程中總是緩衝的原因;同時影片的索引資訊沒有做優化,moov atom放在末尾,播放器解碼速度慢,導致首次等待時間長。
鑑於運營能力,只能先使用壓縮工具減小影片大小。我這邊緊急上線一個下載功能,訓練之前把所有課程章節的影片快取到本地,訓練過程使用本地資源播放技術方面Flutter直接使用dio download按順序下載。

二、與時俱進

通過臨時上線的下載功能,使用者可以順暢訓練;但很快又引出下一個問題:影片大小壓不下,10個章節可能需要5 min+的下載時間,同時檔案完整性沒有做校驗,出錯率較高。如何解決?

  1. 從源頭入手:通過跟剪輯人員的大量交流以及不斷製作demo檢視效果,我們發現一個尺寸為1080 * 1080的mp4,位元速率在2000左右、幀數峰值25,在任何解析度/dpi的手機螢幕上,流暢度和清晰度都完全沒問題。然後再通過專業的壓縮工具,基本每分鐘的影片可以控制在8M以下。因此要求內容團隊的同事按照這個尺寸出影片即可。
  2. 優化下載機制:下載的機制從最簡單粗暴的按順序下載改為:下載第一章節,進入訓練頁面會啟動後臺下載,同時支援斷點下載。
    由此減少使用者的等待下載的時間,同時避免使用者切章節的時候,原先未完成下載的章節作廢,節約了重複下載的流量。(主要也給伺服器節流😊)
    這裡聊下Flutter斷點下載的實現:
    - 明確下載過程中的檔案我們以.mp4.temp字尾名結尾,下載完成的檔案以mp4結尾;
    - 讀取本地快取中此資源未完成下載的檔案長度;
    - 把已下載的長度設定在dio get請求headers的"range": "bytes=$downloadStart-"中;
    - 通過stream把下載進度通知給呼叫方。 Dart Future<void> downloadFile({ required String url, required String savePath, // 本地快取的路徑 required CancelToken cancelToken, // 下載憑證由呼叫方傳入,以操作下載節點(如:取消) ProgressCallback? onReceiveProgress, void Function()? done, void Function(Exception)? failed, }) async { int downloadStart = 0; File f = File(savePath); if (await f.exists()) { downloadStart = f.lengthSync(); } print("start: $downloadStart"); try { var response = await downloadDio.get<ResponseBody>( url, options: Options( /// Receive response data as a stream responseType: ResponseType.stream, followRedirects: false, headers: { /// Downloading key locations in segments "range": "bytes=$downloadStart-", }, ), ); File file = File(savePath); RandomAccessFile raf = file.openSync(mode: FileMode.append); int received = downloadStart; int total = await _getContentLength(response); Stream<Uint8List> stream = response.data!.stream; StreamSubscription<Uint8List>? subscription; subscription = stream.listen( (data) { /// Write files must be synchronized raf.writeFromSync(data); received += data.length; onReceiveProgress?.call(received, total); }, onDone: () async { file.rename(savePath.replaceAll('.temp', '')); await raf.close(); done?.call(); }, onError: (e) async { await raf.close(); failed?.call(e); }, cancelOnError: true, ); cancelToken.whenCancel.then((_) async { await subscription?.cancel(); await raf.close(); }); } on DioError catch (error) { if (CancelToken.isCancel(error)) { print("Download cancelled"); } else { failed?.call(error); } } }
  3. 檔案完整性校驗:使用md5編碼對檔案完整性進行校驗,運營平臺上傳影片時進行編碼,App端下載成功後進也行md5編碼,二者相同則判定檔案完整。 Dart File(path).readAsBytes().then((Uint8List str) { if (md5.convert(str).toString() == md5Str) { // md5Str是服務端返回的編碼 // 二者相同,檔案完整 } });

三、展望未來

經過兩次的優化,基本能滿足目前使用者的使用。但還遠遠不夠,我想對標Keep,達到如下期望:影片秒開、播放流暢、節省流量、提高安全性(比如:擁有自己的編碼格式)、區分碼流(不同尺寸/不同網路情況下選擇最優解)。
image.png 一般情況下,影片流從載入到準備播放過程需要經過解協議、解封裝、解編碼等這樣的過程,其中協議已定,我們用的是HTTP協議;封裝通俗點說就是格式,比如mp4、m3u8、rmvb等;而格式通常是對一套編碼格式的封裝,如MP4的編碼格式大多是h264、h265;m3u8是HLS;RMVB主要是RV40;
- 確定容器
我希望能夠達到區分碼流的效果,客戶端在播放頂級m3u8檔案時,會選擇位元速率高的流,當碼流達不到時會請求位元速率低的流,所以m3u8本身可以直接用於多位元速率影片;因此確定使用m3u8格式進行影片儲存。
如何做到m3u8格式轉換?
1. 運營平臺在上傳影片的時候,使用ffmpeg對原始檔進行編碼,處理適配多碼流外;
2. 服務端選用支援 轉碼 和 動態位元速率 的支援,七牛雲和阿里雲都有類似的支援,通過轉碼統一所有編碼和壓縮,並且提供多位元速率影片。 - App端的實現細節
1. 快取機制是一個難點,m3u8是ts檔案的集合,在播放過程中會出現跳過部分切片的需求,所以ts檔案是不會按順序快取的,因此如何順序匹配是個問題;
2. ts檔案的完整性校驗不通過時,需要請求線上播放,這個容錯機制同樣是個難題;
3. 邊播邊下載的技術實現,這塊我暫時還沒有去了解,但肯定是一個難點😄;
...... - 提高安全性
愛奇藝和騰訊影片,其實都有自己的編碼格式,但目前我們的專案並非完全的影片播放應用,故我認為對切片進行加解密已經足夠了,比如AES-128。 - 影片秒開
影片秒開的優化主要在於一秒內成功載入的播放數/播放總數,我們知道m3u8是由多個切片組成,因此秒開跟第一個切片的大小息息相關,一般第一個ts片段長度控制在1~3秒
影片第一幀(類似關鍵幀的概念)要把畫面補足,最好就是在影片剪輯過程中,在降低位元速率的時候,包住關鍵幀(這個我也是道聽途說,做過影片編輯的運營才能懂了😂)

寫在最後

其實預研下來,目前主流的方案大多包含這些技術點:m3u8格式、hls編碼、AES-128加解密、邊播邊下、關鍵幀控制......

目前主流的庫:android端 gsyvideoplayer、web端 xgplayer,可惜Flutter還沒有遇到很好的庫,目前我們用的是官方的videoPlayer,期待更多的開源了。

對於音影片的探索,我也是逐步學習過程中,期待關於此篇文章更多的討論

非常感謝

GSY大佬親自解答疑惑

位元組同和君大佬鼎力相助

探索移動端音影片與GSYVideoPlayer之旅

ffmpeg 多位元速率m3u8格式轉換