Android 直播流程2()
打包
視音訊在傳輸過程中需要定義相應的格式,這樣傳輸到對端的時候才能正確地被解析出來。
1、HTTP-FLV
Web 2.0時代,要說什麼型別網站最火,自然是以國外的Youtube,國內的優酷、土豆網站了。這類網站提供的視訊內容可謂各有千秋,但它們無一例外的都使用了Flash作為視訊播放載體,支撐這些視訊網站的技術基礎就是——Flash 視訊(FLV) 。FLV 是一種全新的流媒體視訊格式,它利用了網頁上廣泛使用的Flash Player 平臺,將視訊整合到Flash動畫中。也就是說,網站的訪問者只要能看Flash動畫,自然也能看FLV格式視訊,而無需再額外安裝其它視訊外掛,FLV視訊的使用給視訊傳播帶來了極大便利。
HTTP-FLV即將音視訊資料封裝成FLV,然後通過HTTP協議傳輸給客戶端。而作為上傳端只需要將FLV格式的視音訊傳輸到伺服器端即可。
一般來說FLV格式的視音訊,裡面視訊一般使用h264格式,而音訊一般使用AAC-LC格式。
FLV格式是先傳輸FLV頭資訊,然後傳輸帶有視音訊引數的元資料(Metadata),然後傳輸視音訊的引數資訊,然後傳輸視音訊資料。
2、RTMP
RTMP是Real Time Messaging Protocol(實時訊息傳輸協議)的首字母縮寫。該協議基於TCP,是一個協議簇,包括RTMP基本協議及RTMPT/RTMPS/RTMPE等多種變種。RTMP是一種設計用來進行實時資料通訊的網路協議,主要用來在Flash/AIR平臺和支援RTMP協議的流媒體/互動伺服器之間進行音視訊和資料通訊。
RTMP協議是Adobe公司推出的實時傳輸協議,主要用於基於flv格式的音視訊流的實時傳輸。得到編碼後的視音訊資料後,先要進行FLV包裝,然後封包成rtmp格式,然後進行傳輸。
使用RTMP格式進行傳輸,需要先連線伺服器,然後建立流,然後釋出流,然後傳輸相應的視音訊資料。整個傳送是用訊息來定義的,rtmp定義了各種形式的訊息,而為了訊息能夠很好地傳送,又對訊息進行了分塊處理,整個協議較為複雜。
差網路處理
好的網路下視音訊能夠得到及時的傳送,不會造成視音訊資料在本地的堆積,直播效果流暢,延時較小。而在壞的網路環境下,視音訊資料傳送不出去,則需要我們對視音訊資料進行處理。差網路環境下對視音訊資料一般有四種處理方式:快取區設計、網路檢測、丟幀處理、降位元速率處理。
1、緩衝區設計
視音訊資料傳入緩衝區,傳送者從緩衝區獲取資料進行傳送,這樣就形成了一個非同步的生產者消費者模式。生產者只需要將採集、編碼後的視音訊資料推送到緩衝區,而消費者則負責從這個緩衝區裡面取出資料傳送。
2、網路檢測
差網路處理過程中一個重要的過程是網路檢測,當網路變差的時候能夠快速地檢測出來,然後進行相應的處理,這樣對網路反應就比較靈敏,效果就會好很多。
我們這邊通過實時計算每秒輸入緩衝區的資料和傳送出去資料,如果傳送出去的資料小於輸入緩衝區的資料,那麼說明網路頻寬不行,這時候緩衝區的資料會持續增多,這時候就要啟動相應的機制。
3、丟幀處理
當檢測到網路變差的時候,丟幀是一個很好的應對機制。視訊經過編碼後有關鍵幀和非關鍵幀,關鍵幀也就是一副完整的圖片,而非關鍵幀描述影象的相對變化。
丟幀策略多鍾多樣,可以自行定義,一個需要注意的地方是:如果要丟棄P幀(非關鍵幀),那麼需要丟棄兩個關鍵幀之間的所有非關鍵幀,不然的話會出現馬賽克。對於丟幀策略的設計因需求而異,可以自行進行設計。
4、降位元速率
在Android中,如果使用了硬編進行編碼,在差網路環境下,我們可以實時改變硬編的位元速率,從而使直播更為流暢。當檢測到網路環境較差的時候,在丟幀的同時,我們也可以降低視音訊的位元速率。在Android sdk版本大於等於19的時候,可以通過傳遞引數給MediaCodec,從而改變硬編編碼器出來資料的位元速率。
(2)封裝
沿用前面的比喻,封裝可以理解為採用哪種貨車去運輸,也就是媒體的容器。 所謂容器,就是把編碼器生成的多媒體內容(視訊,音訊,字幕,章節資訊等)混合封裝在一起的標準。容器使得不同多媒體內容同步播放變得很簡單,而容器的另一個作用就是為多媒體內容提供索引,也就是說如果沒有容器存在的話一部影片你只能從一開始看到最後,不能拖動進度條,而且如果你不自己去手動另外載入音訊就沒有聲音。
下面是幾種常見的封裝格式:
1)AVI 格式(字尾為 .avi)
2)DV-AVI 格式(字尾為 .avi)
3)QuickTime File Format 格式(字尾為 .mov)
4)MPEG 格式(檔案字尾可以是 .mpg .mpeg .mpe .dat .vob .asf .3gp .mp4等)
5)WMV 格式(字尾為.wmv .asf)
6)Real Video 格式(字尾為 .rm .rmvb)
7)Flash Video 格式(字尾為 .flv)
8)Matroska 格式(字尾為 .mkv)
9)MPEG2-TS 格式 (字尾為 .ts) 目前,我們在流媒體傳輸,尤其是直播中主要採用的就是 FLV 和 MPEG2-TS 格式,分別用於 RTMP/HTTP-FLV 和 HLS 協議。
4.推流到伺服器
推流是直播的第一公里,直播的推流對這個直播鏈路影響非常大,如果推流的網路不穩定,無論我們如何做優化,觀眾的體驗都會很糟糕。所以也是我們排查問題的第一步,如何系統地解決這類問題需要我們對相關理論有基礎的認識。 推送協議主要有三種:
RTSP(Real Time Streaming Protocol):實時流傳送協議,是用來控制聲音或影像的多媒體串流協議, 由Real Networks和Netscape共同提出的; RTMP(Real Time Messaging Protocol):實時訊息傳送協議,是Adobe公司為Flash播放器和伺服器之間音訊、視訊和資料傳輸 開發的開放協議; HLS(HTTP Live Streaming):是蘋果公司(Apple Inc.)實現的基於HTTP的流媒體傳輸協議; RTMP協議基於 TCP,是一種設計用來進行實時資料通訊的網路協議,主要用來在 flash/AIR 平臺和支援 RTMP 協議的流媒體/互動伺服器之間進行音視訊和資料通訊。支援該協議的軟體包括 Adobe Media Server/Ultrant Media Server/red5 等。 它有三種變種:
RTMP工作在TCP之上的明文協議,使用埠1935; RTMPT封裝在HTTP請求之中,可穿越防火牆; RTMPS類似RTMPT,但使用的是HTTPS連線; RTMP 是目前主流的流媒體傳輸協議,廣泛用於直播領域,可以說市面上絕大多數的直播產品都採用了這個協議。 RTMP協議就像一個用來裝資料包的容器,這些資料可以是AMF格式的資料,也可以是FLV中的視/音訊資料。一個單一的連線可以通過不同的通道傳輸多路網路流。這些通道中的包都是按照固定大小的包傳輸的。
伺服器流分發
直播CDN分發網路
CDN,中文名稱是內容分發網路,可以用來分發直播、點播、網頁靜態檔案、小檔案等等,幾乎我們日常用到的網際網路產品都是有CDN在背後提供支援。現在有很多公司在提供雲服務,這是在CDN的基礎上,提供了更豐富的一站式接入的雲服務能力。例如PP雲服務為客戶提供直播、點播、靜態檔案、短視訊等多種雲服務和CDN加速能力。
概念:負載均衡、CDN快取、回源、就近原則
在這樣的架構下,會延伸出這樣的幾個概念:
當觀眾人數不太多的時候,例如總共只有1000人,那麼是選擇讓某一臺伺服器服務這1000人,還是3臺伺服器分擔1000人,還是2臺?機器也會有新舊之分,老機器只能抗800數量,那要怎麼來分配呢?等等問題。這裡就需要有一個策略來做資源的分配。這個策略叫做:負載均衡。
因為觀眾看到的資料都是一樣的,所以呢,資料會在伺服器1、2、3上都儲存一份。這個概念叫做:CDN快取。
當分配到伺服器1的第一個觀眾進入時,伺服器1是沒有儲存資料的,它會向伺服器-0獲取資料,這個過程叫做:回源;相應的,伺服器-0被稱為:源站;觀眾請求的資料如果由CDN快取提供,叫做快取命中,所有使用者請求的快取命中比例叫做快取命中率,它是衡量CDN質量的關鍵指標。
一名新進入的觀眾會被分配到哪一臺伺服器上呢?理論上,這臺伺服器距離使用者的網路鏈路越短、不跨網,資料的傳輸的穩定性就越好,這個叫做:就近原則。
跨地區、多運營商覆蓋的CDN
由於就近原則的存在,為了滿足全國甚至全世界不同地方的人,那我們就需要把伺服器分佈在不同的地區。又由於不同的網路運營商之間的網路傳輸會有穩定性問題,那麼就需要在不同的網路運營商裡也放置伺服器,於是,一個CDN網路就成型了:
傳統直播一般是基於CDN網路進行分發,可支援大規模併發(併發數取決於CDN網路容量)。與傳統CDN的大檔案,小檔案分發不同,由於直播分佈區域分散,一般除了提供播放端的下行分發網路外,還提供上行主播推流匯聚網路。只有一些直播內容資源集中的業務方,會要求直播CDN直接回自己的源站,如電視臺。
上行匯聚
目前傳統直播 CDN 上行一般使用 RTMP 協議,當然也有一些使用 UDP(UDP 方式由於需要 SDK 配合,目前行業內有人在做,但是需要繫結 SDK)。另外國外還有使用 http-ts 的方式進行推流的,可參見 nginx-rtmp 專案大神開源的 nginx-ts-module。當然,目前使用這種方式,關鍵問題還是在於端的支援問題,而該開源專案目前只支援 HLS 和 Dash 的播放。
除了主播推流以外,還有一種方式即從匯聚點到業務方源站去拉流的方式
下行分發
目前下行分發一般使用的協議,rtmp,http-flv,hls 三種協議。這三種協議的優劣,網上已經有很多文章了,一般從終端相容性,延遲,首屏幾個維度去考慮,這裡就不在進行比較。
rtmp 和 http-flv
由於 rtmp 協議在傳送資料前互動次數較多,比較追求首屏的直播平臺一般都會選擇 http-flv 協議作為下行分發協議,線上環境測試效果平均會增加 100-200 ms 左右的時間,網路越差,這個值越大。
rtmp 和 http-flv 的延遲可以做到 3s 以內,但是由於網路環境的複雜,過低的延遲會導致卡頓率的提升,所以一般 CDN 會使用者接入時,給使用者多發幾秒鐘的資料(一般是 5-8s),填充播放端緩衝區,來抗網路端的抖動。細節技術會在後面的文章中介紹
hls
hls 對 Android 端和 IOS 端支援較好,並且對 P2P 的支援也較好,一般對延遲要求不高的直播平臺(如體育賽事)會選用這個協議。
hls 的延遲一般和切片大小有關,一般切片是 6-8s 一個片,這個大小對一般主播推流 GOP 適配最好。過高會導致延遲加大,過低,可能切片裡就沒有關鍵幀。一般 m3u8 檔案裡會有 3 個 ts 檔案,播放器會在下完兩個片以後,開始播放,並且同時下第三個片。因此一般 hls 的時延在 15s 左右。
當然如果使用者調小 GOP(1s),CDN 端將切片方式配置為按 GOP 切片的方式,HLS 實際也可以做到 5s 以內延遲的。當然壞處就是會導致卡頓率變高
拉流播放器播放
視音訊編解碼一般分為兩種,一種是硬編實現,一種是軟編實現。這兩種方式各有優缺點,硬編效能好,但是需要對相容性進行相應處理;軟編相容性好,可以進行一些引數設定,但是軟編一般效能較差,引入相關的編解碼庫往往會增大app的整體體積,而且還需要寫相應的jni介面。
先介紹幾個和視音訊相關的類,通過這幾個類的組合使用,其實是能變換出許多視音訊處理的相關功能
MediaMetadataRetriever::用來獲取視訊的相關資訊,例如視訊寬高、時長、旋轉角度、位元速率等等。
MediaExtractor::視音訊分離器,將一些格式的視訊分離出視訊軌道和音訊軌道。
MediaCodec:視音訊相應的編解碼類。
MediaMuxer:視音訊合成器,將視訊和音訊合成相應的格式。
MediaFormat:視音訊相應的格式資訊。
MediaCodec.BufferInfo:存放ByteBuffer相應資訊的類。
MediaCrypto:視音訊加密解密處理的類。
MediaCodecInfo:視音訊編解碼相關資訊的類。
MediaFormat和MediaCodec.BufferInfo是串起上面幾個類的橋樑,上面幾個視音訊處理的類通過這兩個橋樑建立起聯絡,從而變化出相應的功能
MediaMetadataRetriever
MediaMetadataRetriever用來獲取視音訊的相關資訊,MediaMetadataRetriever的使用十分簡單,傳入相應的檔案路徑建立MediaMetadataRetriever,之後便可以得到視訊的相關引數。
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(file.getAbsolutePath());
String widthString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
if(!TextUtils.isEmpty(widthString)) {
width = Integer.valueOf(widthString);
}
String heightString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
if(!TextUtils.isEmpty(heightString)) {
height = Integer.valueOf(heightString);
}
String durationString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
if(!TextUtils.isEmpty(durationString)) {
duration = Long.valueOf(durationString);
}
String bitrateString = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE);
if(!TextUtils.isEmpty(bitrateString)) {
bitrate = Integer.valueOf(bitrateString);
}
String degreeStr = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (!TextUtils.isEmpty(degreeStr)) {
degree = Integer.valueOf(degreeStr);
}
metadataRetriever.release();
MediaExtractor
MediaExtractor用來對視音訊進行分離,對檔案中的視訊音訊軌道進行分離能做大量的事情,比如說要寫一個播放器,那麼首先的第一個步驟是分離出視訊音訊軌道,然後進行相應的處理。
MediaExtractor的建立和MediaMetadataRetriever一樣十分簡單,只需要傳入相應的檔案路徑。通過getTrackCount()可以得到相應的軌道數量,一般情況下視音訊軌道都有,有些時候可能只有視訊,有些時候可能只有音訊。軌道的序號從0開始,通過getTrackFormat(int index)方法可以得到相應的MediaFormat,而通過MediaFormat可以判斷出軌道是視訊還是音訊。通過selectTrack(int index)方法選擇相應序號的軌道。
public static MediaExtractor createExtractor(String path) throws IOException {
MediaExtractor extractor;
File inputFile = new File(path); // must be an absolute path
if (!inputFile.canRead()) {
throw new FileNotFoundException("Unable to read " + inputFile);
}
extractor = new MediaExtractor();
extractor.setDataSource(inputFile.toString());
return extractor;
}
public static String getMimeTypeFor(MediaFormat format) {
return format.getString(MediaFormat.KEY_MIME);
}
public static int getAndSelectVideoTrackIndex(MediaExtractor extractor) {
for (int index = 0; index < extractor.getTrackCount(); ++index) {
if (isVideoFormat(extractor.getTrackFormat(index))) {
extractor.selectTrack(index);
return index;
}
}
return -1;
}
public static int getAndSelectAudioTrackIndex(MediaExtractor extractor) {
for (int index = 0; index < extractor.getTrackCount(); ++index) {
if (isAudioFormat(extractor.getTrackFormat(index))) {
extractor.selectTrack(index);
return index;
}
}
return -1;
}
public static boolean isVideoFormat(MediaFormat format) {
return getMimeTypeFor(format).startsWith("video/");
}
public static boolean isAudioFormat(MediaFormat format) {
return getMimeTypeFor(format).startsWith("audio/");
}
選擇好一個軌道後,便可以通過相應方法提取出相應軌道的資料。extractor.seekTo(startTime, SEEK_TO_PREVIOUS_SYNC)方法可以直接跳轉到開始解析的位置。extractor.readSampleData(byteBuffer, 0)方法則可以將資料解析到byteBuffer中。extractor.advance()方法則將解析位置進行前移,準備下一次解析。
下面是MediaExtractor一般的使用方法。
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(...);
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance();
}
extractor.release();
extractor = null;
MediaCodec
MediaCodec是Android視音訊裡面最為重要的類,它主要實現的功能是對視音訊進行編解碼處理。在編碼方面,可以對採集的視音訊資料進行編碼處理,這樣的話可以對資料進行壓縮,從而實現以較少的資料量儲存視音訊資訊。在解碼方面,可以解碼相應格式的視音訊資料,從而得到原始的可以渲染的資料,從而實現視音訊的播放。
一般場景下音訊使用的是AAC-LC的格式,而視訊使用的是H264格式。這兩種格式在MediaCodec支援的版本(Api 16)也都得到了很好的支援。在直播過程中,先採集視訊和音訊資料,然後將原始的資料塞給編碼器進行硬編,然後得到相應的編碼後的AAC-LC和H264資料。
在Android系統中,MediaCodec支援的格式有限,在使用MediaCodec之前需要對硬編型別的支援進行檢測,如果MediaCodec支援再進行使用。
1、檢查
在使用硬編編碼器之前需要對編碼器支援的格式進行檢查,在Android中可以使用MediaCodecInfo這個類來獲取系統對視音訊硬編的支援情況。
下面的程式碼是判斷MediaCodec是否支援某個MIME:
private static MediaCodecInfo selectCodec(String mimeType) {
int numCodecs = MediaCodecList.getCodecCount();
for (int i = 0; i < numCodecs; i++) {
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
if (!codecInfo.isEncoder()) {
continue;
}
String[] types = codecInfo.getSupportedTypes();
for (int j = 0; j < types.length; j++) {
if (types[j].equalsIgnoreCase(mimeType)) {
return codecInfo;
}
}
}
return null;
}
根據之前的講述,在Android系統中有著不同的顏色格式,有著各種型別的YUV顏色格式和RGB顏色格式。在攝像頭採集的文章中已經講述,需要設定攝像頭採集的影象顏色格式,一般來說設定為ImageFormat.NV21,之後在攝像頭PreView的回撥中得到相應的影象資料。
在Android系統中不同手機中的編碼器支援著不同的顏色格式,一般情況下並不直接支援NV21的格式,這時候需要將NV21格式轉換成為編碼器支援的顏色格式。在攝像頭採集的文章中已經詳細講述YUV影象格式和相應的儲存規則,YUV影象格式的轉換可以使用ofollow,noindex">LibYuv 。
這裡說一下MediaCodec支援的影象格式。一般來說Android MediaCodec支援如下幾種格式:
/**
* Returns true if this is a color format that this test code understands (i.e. we know how
* to read and generate frames in this format).
*/
private static boolean isRecognizedFormat(int colorFormat) {
switch (colorFormat) {
// these are the formats we know how to handle for this test
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
return true;
default:
return false;
}
}
上面大致統計了Android各種手機MediaCodec支援的各種顏色格式,上面5個型別是比較常用的型別。
另外MediaCodec支援Surface的方式輸入和輸出,當編碼的時候只需要在Surface上進行繪製就可以輸入到編碼器,而解碼的時候可以將解碼影象直接輸出到Surface上,使用起來相當方便,需要在Api 18或以上。
2、建立
當需要使用MediaCodec的時候,首先需要根據視音訊的型別建立相應的MediaCodec。在直播專案中視訊使用了H264,而音訊使用了AAC-LC。在Android中建立直播的音訊編碼器需要傳入相應的MIME,AAC-LC對應的是audio/mp4a-latm,而H264對應的是video/avc。如下的程式碼展示了兩個編碼器的建立,其中視訊編碼器的輸入設定成為了Surface的方式。
//Audio
public static MediaCodec getAudioMediaCodec() throws IOException {
int size = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
MediaFormat format = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 1);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_BIT_RATE, 64 * 1000);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, size);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
//Video
public static MediaCodec getVideoMediaCodec() throws IOException {
int videoWidth = getVideoSize(1280);
int videoHeight = getVideoSize(720);
MediaFormat format = MediaFormat.createVideoFormat("video/avc", videoWidth, videoHeight);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, 1300* 1000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
format.setInteger(MediaFormat.KEY_BITRATE_MODE,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
format.setInteger(MediaFormat.KEY_COMPLEXITY,MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR);
MediaCodec mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
return mediaCodec;
}
// We avoid the device-specific limitations on width and height by using values that
// are multiples of 16, which all tested devices seem to be able to handle.
public static int getVideoSize(int size) {
int multiple = (int) Math.ceil(size/16.0);
return multiple*16;
}