基于 FFmpeg 的跨平臺(tái)播放器實(shí)現(xiàn)
背景
隨著游戲娛樂等直播業(yè)務(wù)的增長,在移動(dòng)端觀看直播的需求也日益迫切。但是移動(dòng)端原生的播放器對(duì)各種直播流的支持卻不是很好。
Android 原生的 MediaPlayer 不支持 flv、hls 直播流,iOS 只支持標(biāo)準(zhǔn)的 HLS 流。本文介紹一種基于 ffplay 框架下的跨平臺(tái)播放器的實(shí)現(xiàn),且兼顧硬解碼的實(shí)現(xiàn)。
播放器原理
直觀的講,我們播放一個(gè)媒體文件一般需要5個(gè)基本模塊,按層級(jí)順序:文件讀取模塊(Source)、解復(fù)用模塊(Demuxer)、視頻頻解碼模塊(Decoder)、色彩空間轉(zhuǎn)換模塊(Color Space Converter)、音視頻渲染模塊(Render)。
數(shù)據(jù)的流向如下圖所示,其中 ffmpeg 框架包含了文件讀取、音視頻解復(fù)用的模塊。

文件讀取模塊(Source)的作用是為下級(jí)解復(fù)用模塊(Demuxer)以包的形式源源不斷的提供數(shù)據(jù)流,對(duì)于下一級(jí)的Demuxer來說,本地文件和網(wǎng)絡(luò)數(shù)據(jù)是一樣的。在ffmpeg框架中,文件讀取模塊可分為3層:
協(xié)議層:pipe,tcp,udp,http等這些具體的本地文件或網(wǎng)絡(luò)協(xié)議
抽象層:URLContext結(jié)構(gòu)來統(tǒng)一表示底層具體的本地文件或網(wǎng)絡(luò)協(xié)議
接口層用:AVIOContext結(jié)構(gòu)來擴(kuò)展URLProtocol結(jié)構(gòu)成內(nèi)部有緩沖機(jī)制的廣泛意義上的文件,并且僅僅由最上層用AVIOContext對(duì)模塊外提供服務(wù),實(shí)現(xiàn)讀媒體文件功能。
解復(fù)用模塊(Demuxer):的作用是識(shí)別文件類型,媒體類型,分離出音頻、視頻、字幕原始數(shù)據(jù)流,打上時(shí)戳信息后傳給下級(jí)的視頻頻解碼模塊(Decoder)??梢院?jiǎn)單的分為兩層,底層是 AVIContext,TCPContext,UDPContext 等等這些具體媒體的解復(fù)用結(jié)構(gòu)和相關(guān)的基礎(chǔ)程序,上層是 AVInputFormat 結(jié)構(gòu)和相關(guān)的程序。
上下層之間由 AVInputFormat 相對(duì)應(yīng)的 AVFormatContext 結(jié)構(gòu)的 priv_data 字段關(guān)聯(lián) AVIContext 或 TCPContext 或 UDPContext 等等具體的文件格式。
AVInputFormat 和具體的音視頻編碼算法格式由 AVFormatContext 結(jié)構(gòu)的 streams 字段關(guān)聯(lián)媒體格式,streams 相當(dāng)于 Demuxer 的 output pin,解復(fù)用模塊分離音視頻裸數(shù)據(jù)通過 streams 傳遞給下級(jí)音視頻解碼器。
視頻頻解碼模塊(Decoder)的作用就是解碼數(shù)據(jù)包,并且把同步時(shí)鐘信息傳遞下去。
色彩空間轉(zhuǎn)換模塊(Color Space Converter)顏色空間轉(zhuǎn)換過濾器的作用是把視頻解碼器解碼出來的數(shù)據(jù)轉(zhuǎn)換成當(dāng)前顯示系統(tǒng)支持的顏色格式
音視頻渲染模塊(Render)的作用就是在適當(dāng)?shù)臅r(shí)間渲染相應(yīng)的媒體,對(duì)視頻媒體就是直接顯示圖像,對(duì)音頻就是播放聲音
跨平臺(tái)實(shí)現(xiàn)
在播放器得5個(gè)模塊中文件讀取模塊(Source)、解復(fù)用模塊(Demuxer)和色彩空間轉(zhuǎn)換模塊(Color Space Converter)這三個(gè)模塊都可以用 ffmpeg 的框架進(jìn)行實(shí)現(xiàn),而 FFmpeg 本身就是跨平臺(tái)的。
因此,實(shí)現(xiàn)跨平臺(tái)的播放器的就需要抽象一層平臺(tái)無關(guān)的音視頻解碼、渲染接口。Android、iOS、Window 等平臺(tái)只需要實(shí)現(xiàn)各自平臺(tái)的渲染、硬件解碼(如果支持的話)就可以構(gòu)建一個(gè)標(biāo)準(zhǔn)的基于 FFmpeg 的播放器了。
下圖是基于ffplay的基本播放流程圖:

圖中紅色部分是需要抽象的接口的,結(jié)構(gòu)如下:

其中 FF_Pipenode.run_sync 視頻解碼線程,默認(rèn)有 libavcodec 的軟解碼實(shí)現(xiàn),其他平臺(tái)可以增加自己的硬解碼實(shí)現(xiàn)。SDL_VideoOut 為視頻渲染抽象層,這里 overlay 可以是 Android的 NativeWindow,或者是 OpenGL 的 Texture。
SDL_AudioOut 是音頻播放抽象層,可以直接操作聲卡驅(qū)動(dòng),SDL2.0 里就支持 ALSA、OSS 接口,當(dāng)然也可以用 Android、iOS SDK 中的音頻 API 實(shí)現(xiàn)。
這里順便提下,隨著 Android、iOS 平臺(tái)的普及,ffmpeg 版本的也逐步支持了 Android、iOS 的硬件解碼器,如f fmpeg 在很早之前就支持了 libstagefright,最新的 ffmpeg2.8 也已經(jīng)支持了 iOS 的硬件解碼庫 VideoToolBox。從下面重點(diǎn)介紹下視頻硬解碼以及音視頻渲染模塊在移動(dòng)平臺(tái)上的實(shí)現(xiàn)。
Android
1.硬解碼模塊:
Android 的硬解碼模塊目前有 2 種實(shí)現(xiàn)方案:
libstagefright_h264:
libstagefright 是 Android2.3 之后版本的多媒體庫,ffmpeg 早在 0.9 版本時(shí)就已經(jīng)將libstagefright_h264 收錄到自己的解碼庫中了,從 libstagefright.cpp 包括的頭文件路徑來看,是基于 Android2.3 版本的源碼。因此編譯 libstagefright 需要 Android2.3 的相關(guān)源碼以及動(dòng)態(tài)鏈接庫。
ffmpeg 中的 libstagefright 目前只實(shí)現(xiàn)了 h264 格式的解碼,由于 Android 機(jī)型、版本的碎片化相當(dāng)嚴(yán)重,這種基于某個(gè) Android 版本編譯出來的 libstagefright 也存在很嚴(yán)重的兼容性問題,我在 Android4.4 的機(jī)型上就遇到無法解碼的問題。
MediaCodec:
MediaCodec 是 Google 在 Android4.1(API16)以后新提供的硬件編解碼 API,其工作原理如圖所示:

以解碼為例,先從 Codec 獲取 inputBuffer,將待解碼數(shù)據(jù)填充到 inputbuffer,再將 inputbuffer 交給Codec,接下來就可以從 Codec 的 outputBuffer 中拿到新鮮出爐的圖像和聲音信息了。下面的這段實(shí)例代碼也許更能說明問題:
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 [現(xiàn)在提供了Native層的 API 了],而我們的播放器是基于 ffplay 架構(gòu)的,核心的解碼模塊是不可能移到 java 層的。
既然我們移不上去,就只能把 MediaCodec 拉到 Native 層了,通過 (*JNIEnv)->CallxxxMethod 的方式將 MediaCodec 相關(guān)的 API 在 Native 層做了一套接口。嗯,現(xiàn)在我們可以來實(shí)現(xiàn)視頻的硬件解碼了:

queue_picture 的實(shí)現(xiàn)如下圖所示:

2.視頻渲染模塊:
在渲染之前,我們必須先指定一個(gè)渲染的畫布,在android上這個(gè)畫布可以是ImageView,SurfaceView,TextureView或者是GLSurfaceView。
關(guān)于在Native層渲染圖片的方法,我曾看過一篇文章,文中介紹了四種渲染方法:
Java Surface JNI
OpenGL ES 2 Texture
NDK ANativeWindow API
Private C++ API
如果是用 ffmpeg 的 libavcodec 進(jìn)行軟解碼,那么使用 NDK ANativeWindow API 將是最高效簡(jiǎn)單的方案,主要實(shí)現(xiàn)代碼:
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 圖像數(shù)據(jù)。
如果是使用了 MediaCodec 進(jìn)行解碼,那么視頻渲染將變得異常簡(jiǎn)單,只需在 MediaCodec 配置時(shí)(MediaCodec.configure)指定圖像渲染的 Surface,然后再解碼完每一幀圖像的時(shí)候調(diào)用 releaseOutputBuffer (index, true),MediaCodec 內(nèi)部就會(huì)將圖像渲染到指定的 Surface 上。
3.音頻播放模塊
Android 支持 2 套音頻接口,分別是 AudioTrack 和 OpenSL ES,這里以 AudioTrack 為例介紹下音頻的部分流程:
由于 AudioTrack 只有 java 層的 API,我們也得像 MediaCodec 一樣在 Native 層重做一套 AudioTrack 的接口。

這里解碼和播放是 2 個(gè)獨(dú)立的線程,audioCallback 負(fù)責(zé)從 Audio Frame queue 中獲取解碼后的音頻數(shù)據(jù),如果解碼后的音頻采樣率不是 AudioTrack 所支持的,就需要用 libswresample 進(jìn)行重采樣。
iOS
1. 硬解碼模塊
從 iOS8 開始,開放了硬解碼和硬編碼 API,就是名為 VideoToolbox.framework 的 API,支持 h264 的硬件編解碼,不過需要 iOS 8 及以上的版本才能使用。這套硬解碼 API 是幾個(gè)純 C 函數(shù),在任何 OC 或者 C++ 代碼里都可以使用。首先要把 VideoToolbox.framework 添加到工程里,并且包含以下頭文件。
解碼主要需要以下四個(gè)函數(shù):
VTDecompositionSessionCreate 創(chuàng)建解碼session
VTDecompressionSessionDecodeFrame 解碼一個(gè)frame
VTDecompressionOutputCallback 解碼完一個(gè)frame后的回調(diào)
VTDecompressionSessionInvalidate 銷毀解碼session
解碼流程如圖所示:

2. 視頻渲染模塊
視頻的渲染采用 OpenGL ES2 紋理貼圖的形式。
3. 音頻播放模塊
采用 iOS 的 AudioToolbox.frameworks 進(jìn)行播放。數(shù)據(jù)流程和 Android 平臺(tái)是相同,不同的是,Android 平臺(tái)把 PCM 數(shù)據(jù)喂給 AudioTrack,iOS 上把 PCM 數(shù)據(jù)喂給 AudioQueue。
總結(jié)
其實(shí) ffpmeg 自帶的播放器實(shí)例 ffplay 就是一個(gè)跨平臺(tái)的播放器,得益于其依賴的多媒體庫 SDL 實(shí)現(xiàn)了多平臺(tái)的音視頻渲染。但是 SDL 庫過于龐大,并不適合整體移植到移動(dòng)端。本文介紹的跨平臺(tái)實(shí)現(xiàn)方案也是借鑒了 SDL2.0 的內(nèi)部實(shí)現(xiàn),只是重新設(shè)計(jì)了渲染接口。
來源:https://cloud.tencent.com/developer/article/1004561
推薦:
Android FFmpeg 實(shí)現(xiàn)帶濾鏡的微信小視頻錄制功能
