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

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

背景:

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

播放器原理:

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

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

    • 協議層: 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)顏色空間轉換過濾器的做用是把視頻解碼器解碼出來的數據轉換成當前顯示系統支持的顏色格式session

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

跨平臺實現

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

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

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

其中 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 的內部實現,只是從新設計了渲染接口。

相關文章
相關標籤/搜索