1. 程式人生 > >基於 ffmpeg 的跨平臺播放器實現

基於 ffmpeg 的跨平臺播放器實現

空間 編解碼 流程 position eat clu ict 網絡協議 紋理貼圖

https://www.qcloud.com/community/article/309889001486708756

背景:

隨著遊戲娛樂等直播業務的增長,在移動端觀看直播的需求也日益迫切。但是移動端原生的播放器對各種直播流的支持卻不是很好。Android 原生的 MediaPlayer 不支持 flv、hls 直播流,iOS 只支持標準的 HLS 流。本文介紹一種基於 ffplay 框架下的跨平臺播放器的實現,且兼顧硬解碼的實現。

播放器原理:

直觀的講,我們播放一個媒體文件一般需要5個基本模塊,按層級順序:文件讀取模塊(Source)、解復用模塊(Demuxer)、視頻頻解碼模塊(Decoder)、色彩空間轉換模塊(Color Space Converter)、音視頻渲染模塊(Render)。數據的流向如下圖所示,其中 ffmpeg 框架包含了文件讀取、音視頻解復用的模塊。

技術分享

  1. 文件讀取模塊(Source)的作用是為下級解復用模塊(Demuxer)以包的形式源源不斷的提供數據流,對於下一級的Demuxer來說,本地文件和網絡數據是一樣的。在ffmpeg框架中,文件讀取模塊可分為3層:

    • 協議層: pipe,tcp,udp,http等這些具體的本地文件或網絡協議
    • 抽象層:URLContext結構來統一表示底層具體的本地文件或網絡協議
    • 接口層用:AVIOContext結構來擴展URLProtocol結構成內部有緩沖機制的廣泛意義上的文件,並且僅僅由最上層用AVIOContext對模塊外提供服務,實現讀媒體文件功能。
  2. 解復用模塊(Demuxer):的作用是識別文件類型,媒體類型,分離出音頻、視頻、字幕原始數據流,打上時戳信息後傳給下級的視頻頻解碼模塊(Decoder)。可以簡單的分為兩層,底層是 AVIContext,TCPContext,UDPContext 等等這些具體媒體的解復用結構和相關的基礎程序,上層是 AVInputFormat 結構和相關的程序。上下層之間由 AVInputFormat 相對應的 AVFormatContext 結構的 priv_data 字段關聯 AVIContext 或 TCPContext 或 UDPContext 等等具體的文件格式。AVInputFormat 和具體的音視頻編碼算法格式由 AVFormatContext 結構的 streams 字段關聯媒體格式,streams 相當於 Demuxer 的 output pin,解復用模塊分離音視頻裸數據通過 streams 傳遞給下級音視頻解碼器。

  3. 視頻頻解碼模塊(Decoder)的作用就是解碼數據包,並且把同步時鐘信息傳遞下去。

  4. 色彩空間轉換模塊(Color Space Converter)顏色空間轉換過濾器的作用是把視頻解碼器解碼出來的數據轉換成當前顯示系統支持的顏色格式

  5. 音視頻渲染模塊(Render)的作用就是在適當的時間渲染相應的媒體,對視頻媒體就是直接顯示圖像,對音頻就是播放聲音

跨平臺實現

在播放器得5個模塊中文件讀取模塊(Source)、解復用模塊(Demuxer)和色彩空間轉換模塊(Color Space Converter)這三個模塊都可以用 ffmpeg 的框架進行實現,而f fmpeg 本身就是跨平臺的。因此,實現跨平臺的播放器的就需要抽象一層平臺無關的音視頻解碼、渲染接口。Android、iOS、Window 等平臺只需要實現各自平臺的渲染、硬件解碼(如果支持的話)就可以構建一個標準的基於 ffmpeg 的播放器了。

下圖是基於ffplay的基本播放流程圖:

技術分享

圖中紅色部分是需要抽象的接口的,結構如下:

技術分享

其中 FF_Pipenode.run_sync 視頻解碼線程,默認有 libavcodec 的軟解碼實現,其他平臺可以增加自己的硬解碼實現。SDL_VideoOut 為視頻渲染抽象層,這裏 overlay 可以是 Android的 NativeWindow,或者是 OpenGL 的 Texture。SDL_AudioOut 是音頻播放抽象層,可以直接操作聲卡驅動,SDL2.0 裏就支持 ALSA、OSS 接口,當然也可以用 Android、iOS SDK 中的音頻 API 實現。

這裏順便提下,隨著 Android、iOS 平臺的普及,ffmpeg 版本的也逐步支持了 Android、iOS 的硬件解碼器,如f fmpeg 在很早之前就支持了 libstagefright,最新的 ffmpeg2.8 也已經支持了 iOS 的硬件解碼庫 VideoToolBox。從下面重點介紹下視頻硬解碼以及音視頻渲染模塊在移動平臺上的實現。

Android

1.硬解碼模塊:

Android 的硬解碼模塊目前有 2 種實現方案:

libstagefright_h264:

libstagefright 是 Android2.3 之後版本的多媒體庫,ffmpeg 早在 0.9 版本時就已經將libstagefright_h264 收錄到自己的解碼庫中了,從 libstagefright.cpp 包括的頭文件路徑來看,是基於 Android2.3 版本的源碼。因此編譯 libstagefright 需要 Android2.3 的相關源碼以及動態鏈接庫。

ffmpeg 中的 libstagefright 目前只實現了 h264 格式的解碼,由於 Android 機型、版本的碎片化相當嚴重,這種基於某個 Android 版本編譯出來的 libstagefright 也存在很嚴重的兼容性問題,我在 Android4.4 的機型上就遇到無法解碼的問題。

MediaCodec:

MediaCodec 是 Google 在 Android4.1(API16)以後新提供的硬件編解碼 API,其工作原理如圖所示:

技術分享

以解碼為例,先從 Codec 獲取 inputBuffer,將待解碼數據填充到 inputbuffer,再將 inputbuffer 交給Codec,接下來就可以從 Codec 的 outputBuffer 中拿到新鮮出爐的圖像和聲音信息了。下面的這段實例代碼也許更能說明問題:

MediaCodec codec = MediaCodec.createByCodecName(name);
 codec.configure(format, …);
 MediaFormat outputFormat = codec.getOutputFormat(); // option B
 codec.start();
 for (;;) {
   int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
   if (inputBufferId >= 0) {
     ByteBuffer inputBuffer = codec.getInputBuffer(…);
     // fill inputBuffer with valid data
     …
     codec.queueInputBuffer(inputBufferId, …);
   }
   int outputBufferId = codec.dequeueOutputBuffer(…);
   if (outputBufferId >= 0) {
     ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
     MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
     // bufferFormat is identical to outputFormat
     // outputBuffer is ready to be processed or rendered.
     …
     codec.releaseOutputBuffer(outputBufferId, …);
   } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
     // Subsequent data will conform to new format.
     // Can ignore if using getOutputFormat(outputBufferId)
     outputFormat = codec.getOutputFormat(); // option B
   }
 }
 codec.stop();
 codec.release();

令人沮喪的是,MediaCodec 只提供了 java 層的 API,而我們的播放器是基於 ffplay 架構的,核心的解碼模塊是不可能移到 java 層的。既然我們移不上去,就只能把 MediaCodec 拉到 Native 層了,通過 (*JNIEnv)->CallxxxMethod 的方式將 MediaCodec 相關的 API 在 Native 層做了一套接口。嗯,現在我們可以來實現視頻的硬件解碼了:

技術分享

queue_picture 的實現如下圖所示:

技術分享

2.視頻渲染模塊:

在渲染之前,我們必須先指定一個渲染的畫布,在android上這個畫布可以是ImageView,SurfaceView,TextureView或者是GLSurfaceView。

關於在Native層渲染圖片的方法,我曾看過一篇文章,文中介紹了四種渲染方法:

  • Java Surface JNI
  • OpenGL ES 2 Texture
  • NDK ANativeWindow API
  • Private C++ API

如果是用 ffmpeg 的 libavcodec 進行軟解碼,那麽使用 NDK ANativeWindow API 將是最高效簡單的方案,主要實現代碼:

ANativeWindow* window = ANativeWindow_fromSurface(env, javaSurface);

ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, NULL) == 0) {
  memcpy(buffer.bits, pixels,  w * h * 2);
  ANativeWindow_unlockAndPost(window);
}

ANativeWindow_release(window);

示例代碼中的 javaSurface 來自 java 層的 SurfaceHolder,pixels 指向 RGB 圖像數據。

如果是使用了 MediaCodec 進行解碼,那麽視頻渲染將變得異常簡單,只需在 MediaCodec 配置時(MediaCodec.configure)指定圖像渲染的 Surface,然後再解碼完每一幀圖像的時候調用 releaseOutputBuffer (index, true),MediaCodec 內部就會將圖像渲染到指定的 Surface 上。

3.音頻播放模塊

Android 支持 2 套音頻接口,分別是 AudioTrack 和 OpenSL ES,這裏以 AudioTrack 為例介紹下音頻的部分流程:

由於 AudioTrack 只有 java 層的 API,我們也得像 MediaCodec 一樣在 Native 層重做一套 AudioTrack 的接口。

技術分享

這裏解碼和播放是 2 個獨立的線程,audioCallback 負責從 Audio Frame queue 中獲取解碼後的音頻數據,如果解碼後的音頻采樣率不是 AudioTrack 所支持的,就需要用 libswresample 進行重采樣。

iOS

1. 硬解碼模塊

從 iOS8 開始,開放了硬解碼和硬編碼 API,就是名為 VideoToolbox.framework 的 API,支持 h264 的硬件編解碼,不過需要 iOS 8 及以上的版本才能使用。這套硬解碼 API 是幾個純 C 函數,在任何 OC 或者 C++ 代碼裏都可以使用。首先要把 VideoToolbox.framework 添加到工程裏,並且包含以下頭文件。

#include

解碼主要需要以下四個函數:

  • VTDecompositionSessionCreate 創建解碼session
  • VTDecompressionSessionDecodeFrame 解碼一個frame
  • VTDecompressionOutputCallback 解碼完一個frame後的回調
  • VTDecompressionSessionInvalidate 銷毀解碼session

解碼流程如圖所示:

技術分享

2. 視頻渲染模塊

視頻的渲染采用 OpenGL ES2 紋理貼圖的形式。

3. 音頻播放模塊

采用 iOS 的 AudioToolbox.frameworks 進行播放。數據流程和 Android 平臺是相同,不同的是,Android 平臺把 PCM 數據餵給 AudioTrack,iOS 上把 PCM 數據餵給 AudioQueue。

總結

其實 ffpmeg 自帶的播放器實例 ffplay 就是一個跨平臺的播放器,得益於其依賴的多媒體庫 SDL 實現了多平臺的音視頻渲染。但是 SDL 庫過於龐大,並不適合整體移植到移動端。本文介紹的跨平臺實現方案也是借鑒了 SDL2.0 的內部實現,只是重新設計了渲染接口。

基於 ffmpeg 的跨平臺播放器實現