前言
本文主要介紹直播所需要的編解碼基礎(chǔ)彻桃,后續(xù)文章將繼續(xù)介紹實際的運用菇绵。
什么是碼?
這里的碼指碼流(Data Rate)钉鸯,是指視頻文件在單位時間內(nèi)使用的數(shù)據(jù)流量,也叫碼率或碼流率邮辽,通俗一點的理解就是取樣率,是視頻編碼中畫面質(zhì)量控制中最重要的部分唠雕,一般我們用的單位是kb/s或者Mb/s。一般來說同樣分辨率下吨述,視頻文件的碼流越大岩睁,壓縮比就越小,畫面質(zhì)量就越高揣云。碼流越大捕儒,說明單位時間內(nèi)取樣率越大,數(shù)據(jù)流邓夕,精度就越高刘莹,處理出來的文件就越接近原始文件,圖像質(zhì)量越好翎迁,畫質(zhì)越清晰栋猖,要求播放設(shè)備的解碼能力也越高。
編碼
編碼又分為硬編碼和軟編碼汪榔,下面會著重介紹蒲拉。
整個視頻直播分為以下幾個步驟:
采集—>處理—>編碼和封裝—>推流到服務器—>服務器流分發(fā)—>播放器流播放
如下圖所示:
如果把整個流媒體比喻成一個物流系統(tǒng),那么編解碼就是其中配貨和裝貨的過程痴腌,這個過程非常重要雌团,它的速度和壓縮比對物流系統(tǒng)的意義非常大,影響物流系統(tǒng)的整體速度和成本士聪。同樣锦援,對流媒體傳輸來說,編碼也非常重要剥悟,它的編碼性能灵寺、編碼速度和編碼壓縮比會直接影響整個流媒體傳輸?shù)挠脩趔w驗和傳輸成本。
為什么要編碼区岗?
如上所示略板,10Mbps的帶寬傳輸上述7s視頻需要11分鐘,而經(jīng)過H.264編碼之后慈缔,傳輸只需要500ms叮称,時間上前后差距達1000多倍,可以滿足實時傳輸?shù)男枨螅詮囊曨l采集傳感器采集來的原始視頻勢必要經(jīng)過視頻編碼瓤檐。
基本原理
為什么巨大的原始視頻可以編碼成很小的視頻呢?這其中的技術(shù)是什么呢?核心思想就是去除冗余信息
- 空間冗余:圖像相鄰像素之間有較強的相關(guān)性
- 時間冗余:視頻序列的相鄰圖像之間內(nèi)容相似
- 編碼冗余:不同像素值出現(xiàn)的概率不同
- 視覺冗余:人的視覺系統(tǒng)對某些細節(jié)不敏感
- 知識冗余:規(guī)律性的結(jié)構(gòu)可由先驗知識和背景知識得到
編碼標準的選擇
經(jīng)過數(shù)十年的發(fā)展赂韵,從開始的只支持幀內(nèi)編碼演進到現(xiàn)如今的H.265和VP9為代表的新一代編碼標準,下面是一些常見的視頻編碼標準:
- H.264/AVC
- HEVC/H.265
- VP8
- VP9
- FFmpeg
- ...
下面是一些常見的音頻編碼標準
- MP3
- AAC
- WMA
- PCM
- ...
封裝
封裝就是媒體的容器挠蛉,而容器就是把編碼器生成的多媒體內(nèi)容(視頻祭示,音頻,字幕谴古,章節(jié)信息等)混合封裝在一起的標準绍移。容器使得不同多媒體內(nèi)容同步播放變得很簡單,而容器的另一個作用就是為多媒體內(nèi)容提供索引,也就是說如果沒有容器存在的話一部影片你只能從一開始看到最后贮庞,不能拖動進度條参萄,而且打開的視頻也沒有聲音。下面是幾種常見的封裝格式:
- AVI 格式(后綴為 .avi)
- DV-AVI 格式(后綴為 .avi)
- QuickTime File Format 格式(后綴為 .mov)
- MPEG 格式(文件后綴可以是 .mpg .mpeg .mpe .dat .vob .asf .3gp .mp4等)
- WMV 格式(后綴為.wmv .asf)
- Real Video 格式(后綴為 .rm .rmvb)
- Flash Video 格式(后綴為 .flv)
- Matroska 格式(后綴為 .mkv)
- MPEG2-TS 格式 (后綴為 .ts)
目前焦履,我們在流媒體傳輸,尤其是直播中主要采用的就是 FLV 和 MPEG2-TS 格式,分別用于 RTMP/HTTP-FLV 和 HLS 協(xié)議纠炮。
硬件編碼
在Android 4.1 之前,Android是沒有提供硬件編碼的API灯蝴,所以之前都是采用FFMPEG來進行軟編碼的恢口,而FFMPEG都是用C語言開發(fā)的,對于閱讀源碼及學習都有一定的門檻穷躁。對于同一平臺耕肩,同一硬件環(huán)境,硬件編碼速度快于軟件編碼问潭,CPU占有率更低猿诸,所以能用硬件編碼的條件下,我們盡量用硬件編碼狡忙。
MediaCodec
MediaCodec類可用于訪問Android底層的多媒體編解碼器梳虽,例如,編碼器/解碼器組件灾茁。它是Android底層多媒體支持基礎(chǔ)架構(gòu)的一部分(通常與MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)窜觉。
從廣義上講,編解碼器就是處理輸入數(shù)據(jù)來產(chǎn)生輸出數(shù)據(jù)北专。MediaCodec采用異步方式處理數(shù)據(jù)禀挫,并且使用了一組輸入輸出緩存(input and output buffers)。簡單來講逗余,你請求或接收到一個空的輸入緩存(input buffer)特咆,向其中填充滿數(shù)據(jù)并將它傳遞給編解碼器處理。編解碼器處理完這些數(shù)據(jù)并將處理結(jié)果輸出至一個空的輸出緩存(output buffer)中。最終腻格,你請求或接收到一個填充了結(jié)果數(shù)據(jù)的輸出緩存(output buffer)画拾,使用完其中的數(shù)據(jù),并將其釋放給編解碼器再次使用菜职。
數(shù)據(jù)類型
編解碼器可以處理三種類型的數(shù)據(jù):
- 壓縮數(shù)據(jù)(即為經(jīng)過H254. H265. 等編碼的視頻數(shù)據(jù)或AAC等編碼的音頻數(shù)據(jù))
- 原始音頻數(shù)據(jù)
- 原始視頻數(shù)據(jù)青抛。
三種類型的數(shù)據(jù)均可以利用ByteBuffers進行處理,但是對于原始視頻數(shù)據(jù)應提供一個Surface以提高編解碼器的性能酬核。Surface直接使用本地視頻數(shù)據(jù)緩存(native video buffers)蜜另,而沒有映射或復制數(shù)據(jù)到ByteBuffers,因此嫡意,這種方式會更加高效举瑰。在使用Surface的時候,通常不能直接訪問原始視頻數(shù)據(jù)蔬螟,但是可以使用ImageReader類來訪問非安全的解碼(原始)視頻幀此迅。這仍然比使用ByteBuffers更加高效,因為一些本地緩存(native buffer)可以被映射到 direct ByteBuffers旧巾。當使用ByteBuffer模式耸序,你可以利用Image類和getInput/OutputImage(int)方法來訪問到原始視頻數(shù)據(jù)幀。
壓縮緩存
輸入緩存(對于解碼器)和輸出緩存(對于編碼器)中包含由多媒體格式類型決定的壓縮數(shù)據(jù)鲁猩。對于視頻類型是單個壓縮的視頻幀坎怪。對于音頻數(shù)據(jù)通常是單個可訪問單元(一個編碼的音頻片段,通常包含幾毫秒的遵循特定格式類型的音頻數(shù)據(jù))廓握,但這種要求也不是十分嚴格搅窿,一個緩存內(nèi)可能包含多個可訪問的音頻單元。在這兩種情況下隙券,緩存不會在任意的字節(jié)邊界上開始或結(jié)束戈钢,而是在幀或可訪問單元的邊界上開始或結(jié)束。
原始音頻緩存(Raw Audio Buffers)
原始的音頻數(shù)據(jù)緩存包含完整的PCM(脈沖編碼調(diào)制)音頻數(shù)據(jù)幀是尔,這是每一個通道按照通道順序的一個樣本殉了。每一個樣本是一個按照本機字節(jié)順序的16位帶符號整數(shù)(16-bit signed integer in native byte order)。
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
MediaFormat format = codec.getOutputFormat(bufferId);
ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
if (channelIx < 0 || channelIx >= numChannels) {
return null;
}
short[] res = new short[samples.remaining() / numChannels];
for (int i = 0; i < res.length; ++i) {
res[i] = samples.get(i * numChannels + channelIx);
}
return res;
}
原始視頻緩存(Raw Video Buffers)
在ByteBuffer模式下拟枚,視頻緩存(video buffers)根據(jù)它們的顏色格式(color format)進行展現(xiàn)薪铜。你可以通過調(diào)用getCodecInfo().getCapabilitiesForType(…).colorFormats方法獲得編解碼器支持的顏色格式數(shù)組。視頻編解碼器可以支持三種類型的顏色格式:
- 本地原始視頻格式(native raw video format):這種格式通過COLOR_FormatSurface標記恩溅,并可以與輸入或輸出Surface一起使用隔箍。
- 靈活的YUV緩存(flexible YUV buffers)(例如:COLOR_FormatYUV420Flexible):利用一個輸入或輸出Surface,或在在ByteBuffer模式下脚乡,可以通過調(diào)用getInput/OutputImage(int)方法使用這些格式蜒滩。
- 其他滨达,特定的格式(other, specific formats):通常只在ByteBuffer模式下被支持。有些顏色格式是特定供應商指定的俯艰。其他的一些被定義在 MediaCodecInfo.CodecCapabilities中捡遍。這些顏色格式同 flexible format相似,你仍然可以使用 getInput/OutputImage(int)方法竹握。
從Android 5.1(LOLLIPOP_MR1)開始画株,所有的視頻編解碼器都支持靈活的YUV4:2:0緩存(flexible YUV 4:2:0 buffers)。
狀態(tài)(state)
在編解碼器的生命周期內(nèi)有三種理論狀態(tài):停止態(tài)-Stopped啦辐、執(zhí)行態(tài)-Executing谓传、釋放態(tài)-Released,停止狀態(tài)(Stopped)包括了三種子狀態(tài):未初始化(Uninitialized)芹关、配置(Configured)续挟、錯誤(Error)。執(zhí)行狀態(tài)(Executing)在概念上會經(jīng)歷三種子狀態(tài):刷新(Flushed)侥衬、運行(Running)庸推、流結(jié)束(End-of-Stream)。
- 當你使用任意一種工廠方法(factory methods)創(chuàng)建了一個編解碼器浇冰,此時編解碼器處于未初始化狀態(tài)(Uninitialized)。首先聋亡,你需要使用configure(…)方法對編解碼器進行配置肘习,這將使編解碼器轉(zhuǎn)為配置狀態(tài)(Configured)。然后調(diào)用start()方法使其轉(zhuǎn)入執(zhí)行狀態(tài)(Executing)坡倔。在這種狀態(tài)下你可以通過上述的緩存隊列操作處理數(shù)據(jù)漂佩。
- 執(zhí)行狀態(tài)(Executing)包含三個子狀態(tài): 刷新(Flushed)、運行( Running) 以及流結(jié)束(End-of-Stream)罪塔。在調(diào)用start()方法后編解碼器立即進入刷新子狀態(tài)(Flushed)投蝉,此時編解碼器會擁有所有的緩存。一旦第一個輸入緩存(input buffer)被移出隊列征堪,編解碼器就轉(zhuǎn)入運行子狀態(tài)(Running)瘩缆,編解碼器的大部分生命周期會在此狀態(tài)下度過。當你將一個帶有end-of-stream 標記的輸入緩存入隊列時佃蚜,編解碼器將轉(zhuǎn)入流結(jié)束子狀態(tài)(End-of-Stream)庸娱。在這種狀態(tài)下,編解碼器不再接收新的輸入緩存谐算,但它仍然產(chǎn)生輸出緩存(output buffers)直到end-of- stream標記到達輸出端熟尉。你可以在執(zhí)行狀態(tài)(Executing)下的任何時候通過調(diào)用flush()方法使編解碼器重新返回到刷新子狀態(tài)(Flushed)。
- 通過調(diào)用stop()方法使編解碼器返回到未初始化狀態(tài)(Uninitialized)洲脂,此時這個編解碼器可以再次重新配置 斤儿。當你使用完編解碼器后,你必須調(diào)用release()方法釋放其資源。
- 在極少情況下編解碼器會遇到錯誤并進入錯誤狀態(tài)(Error)往果。這個錯誤可能是在隊列操作時返回一個錯誤的值或者有時候產(chǎn)生了一個異常導致的疆液。通過調(diào)用 reset()方法使編解碼器再次可用。你可以在任何狀態(tài)調(diào)用reset()方法使編解碼器返回到未初始化狀態(tài)(Uninitialized)棚放。否則枚粘,調(diào)用 release()方法進入最終的Released狀態(tài)。
數(shù)據(jù)處理(Data Processing)
每一個編解碼器都包含一組輸入和輸出緩存(input and output buffers)飘蚯,這些緩存在API調(diào)用中通過buffer-id進行引用馍迄。當成功調(diào)用start()方法后客戶端將不會“擁有”輸入或輸出buffers。在同步模式下局骤,通過調(diào)用dequeue Input/OutputBuffer(…) 方法從編解碼器獲得一個輸入或輸出buffer的所有權(quán)攀圈。在異步模式下,你可以通過MediaCodec.Callback.onInput/OutputBufferAvailable(…)的回調(diào)方法自動地獲得可用的buffers峦甩。
在獲得一個輸入buffer后赘来,向其中填充數(shù)據(jù),并利用queueInputBuffer方法將其提交給編解碼器凯傲,若使用解密犬辰,則利用queueSecureInputBuffer方法提交。不要提交多個具有相同時間戳的輸入buffers(除非它是也被同樣標記的codec-specific data)冰单。
在異步模式下通過onOutputBufferAvailable方法的回調(diào)或者在同步模式下響應dequeueOutputBuffer的調(diào)用幌缝,編解碼器返回一個只讀的output buffer。在這個output buffer被處理后诫欠,調(diào)用一個releaseOutputBuffer方法將這個buffer返回給編解碼器涵卵。
當你不需要立即向編解碼器重新提交或釋放buffers時,保持對輸入或輸出buffers的所有權(quán)可使編解碼器停止工作荒叼,當然這些行為依賴于設(shè)備情況轿偎。特別地,編解碼器可能延遲產(chǎn)生輸出buffers直到輸出的buffers被釋放或重新提交被廓。因此坏晦,盡可能短時間地持有可用的buffers。根據(jù)API版本情況嫁乘,你有三種處理相關(guān)數(shù)據(jù)的方式:
- Synchronous API using buffer arrays(Android 5.0之前英遭,之后棄用)
- Synchronous API using buffers(適用于Android 5.0之后)
- Asynchronous API using buffers(適用于Android 5.0之后)
使用緩存的異步處理方式(Asynchronous Processing using Buffers)
從Android 5.0(LOLLIPOP)開始,首選的方法是調(diào)用configure之前通過設(shè)置回調(diào)異步地處理數(shù)據(jù)亦渗。異步模式稍微改變了狀態(tài)轉(zhuǎn)換方式挖诸,因為你必須在調(diào)用flush()方法后再調(diào)用start()方法才能使編解碼器的狀態(tài)轉(zhuǎn)換為Running子狀態(tài)并開始接收輸入buffers。同樣法精,初始調(diào)用start方法將編解碼器的狀態(tài)直接變化為Running 子狀態(tài)并通過回調(diào)方法開始傳遞可用的輸入buffers多律。
異步模式下痴突,典型的使用示例如下:
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback(new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
…
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();
使用緩存的同步處理方式(Synchronous Processing using Buffers)
從Android5.0(LOLLIPOP)開始,即使在同步模式下使用編解碼器你應該通過getInput/OutputBuffer(int) 或 getInput/OutputImage(int) 方法檢索輸入和輸出buffers狼荞。這允許通過框架進行某些優(yōu)化辽装,例如,在處理動態(tài)內(nèi)容過程中相味。如果你調(diào)用getInput/OutputBuffers()方法這種優(yōu)化方式是不可用的拾积。
注意,不要同時混淆使用緩存和緩存數(shù)組的方法丰涉。特別地拓巧,僅僅在調(diào)用start()方法后或取出一個值為INFO_OUTPUT_FORMAT_CHANGED的輸出buffer ID后你才可以直接調(diào)用getInput/OutputBuffers方法。
同步模式下MediaCodec的典型應用如下:
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(…);
// 通過有效數(shù)據(jù)來裝填inputBuffer
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// 緩存格式等同于輸出格式
// 輸出格式已經(jīng)準備好了執(zhí)行和渲染
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//數(shù)據(jù)流會遵循新的格式
//如果使用getOutputFormat(outputBufferId)一死,則會被忽略
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
使用緩存數(shù)組的同步處理方式(Synchronous Processing using Buffer Arrays)-- (deprecated)
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(…);
if (inputBufferId >= 0) {
// 通過有效數(shù)據(jù)來裝填I(lǐng)nputBuffers[inputBufferId]
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId]已經(jīng)準備好了被執(zhí)行或渲染
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// 數(shù)據(jù)流會遵循新的格式
MediaFormat format = codec.getOutputFormat();
}
}
codec.stop();
codec.release();