Android系統音視訊架構
1、Android系統上的音訊框架
一個好的系統架構需要儘可能地降低上層與具體硬體的耦合,這既是涉及作業系統的目的,對於音訊系統也是如此。音訊系統的雛形框架可以簡單的用下圖來表示:
在這個圖中,除去Linux本身的Audio驅動外,整個Android音訊實現都被看成了User。因而我們可以認為Audio Driver就是上層與硬體間的“隔離板”。但是如果單純採用上圖所示的框架來設計音訊系統,對上層應用使用音訊功能是不小的負擔,顯然Android開發團隊還會根據自身的實際情況來進一步細化“User”部分。
細化的根據還是Android的幾個層次結構,包括應用層、framework層、庫層以及HAL層,如下圖所示:
Framework
相信大家可以馬上想到MediaPlayer和MediaRecorder,因為這是我們在開發音訊相關產品時使用最廣泛的兩個類。實際上,Android也提供了另兩個相似功能的類,即AudioTrack和AudioRecorder,MediaPlayerService內部的實現就是通過它們來完成的,只不過MediaPlayer/MediaRecorder提供了更強大的控制功能,相比前者也更易於使用。我們後面還會有詳細介紹。
除此以外,Android系統還為我們控制音訊系統提供了AudioManager、AudioService及AudioSystem類。這些都是framework為便利上層應用開發所設計的。
Libraries
我們知道,framework層的很多類,實際上只是應用程式使用Android庫檔案的“中介”而已。因為上層應用採用java語言編寫,它們需要最直接的java介面的支援,這就是framework層存在的意義之一。而作為“中介”,它們並不會真正去實現具體的功能,或者只實現其中的一部分功能,而把主要重心放在庫中來完成。比如上面的AudioTrack、AudioRecorder、MediaPlayer和MediaRecorder等等在庫中都能找到相對應的類。這一部分程式碼集中放置在工程的frameworks/av/media/libmedia中,多數是C++語言編寫的。
除了上面的類庫實現外,音訊系統還需要一個“核心中控”,或者用Android中通用的實現來講,需要一個系統服務(比如ServiceManager、LocationManagerService、ActivityManagerService等等),這就是AudioFlinger和AudioPolicyService。它們的程式碼放置在frameworks/av/services/audioflinger,生成的最主要的庫叫做libaudioflinger。
音訊體系中另一個重要的系統服務是MediaPlayerService,它的位置在frameworks/av/media/libmediaplayerservice。
因為涉及到的庫和相關類是非常多的,建議大家在理解的時候分為兩條線索。
其一,以庫為線索。比如AudioPolicyService和AudioFlinger都是在libaudioflinger庫中;而AudioTrack、AudioRecorder等一系列實現則在libmedia庫中。
其二,以程序為線索。庫並不代表一個程序,程序則依賴於庫來執行。雖然有的類是在同一個庫中實現的,但並不代表它們會在同一個程序中被呼叫。比如AudioFlinger和AudioPolicyService都駐留於名為mediaserver的系統程序中;而AudioTrack/AudioRecorder和MediaPlayer/MediaRecorder一樣實際上只是應用程序的一部分,它們通過binder服務來與其它系統程序通訊。
在分析原始碼的過程中,一定要緊抓這兩條線索,才不至於覺得混亂。
HAL
從設計上來看,硬體抽象層是AudioFlinger直接訪問的物件。這說明了兩個問題,一方面AudioFlinger並不直接呼叫底層的驅動程式;另一方面,AudioFlinger上層(包括和它同一層的MediaPlayerService)的模組只需要與它進行互動就可以實現音訊相關的功能了。因而我們可以認為AudioFlinger是Android音訊系統中真正的“隔離板”,無論下面如何變化,上層的實現都可以保持相容。
音訊方面的硬體抽象層主要分為兩部分,即AudioFlinger和AudioPolicyService。實際上後者並不是一個真實的裝置,只是採用虛擬裝置的方式來讓廠商可以方便地定製出自己的策略。
抽象層的任務是將AudioFlinger/AudioPolicyService真正地與硬體裝置關聯起來,但又必須提供靈活的結構來應對變化——特別是對於Android這個更新相當頻繁的系統。比如以前Android系統中的Audio系統依賴於ALSA-lib,但後期就變為了tinyalsa,這樣的轉變不應該對上層造成破壞。因而Audio HAL提供了統一的介面來定義它與AudioFlinger/AudioPolicyService之間的通訊方式,這就是audio_hw_device、audio_stream_in及audio_stream_out等等存在的目的, 這些Struct資料型別內部大多隻是函式指標的定義,是一些“殼”。當AudioFlinger/AudioPolicyService初始化時,它們會去尋找系統中最匹配的實現(這些實現駐留在以audio.primary., audio.a2dp. 為名的各種庫中)來填充這些“殼”。
根據產品的不同,音訊裝置存在很大差異,在Android的音訊架構中,這些問題都是由HAL層的audio.primary等等庫來解決的,而不需要大規模地修改上層實現。換句話說,廠商在定製時的重點就是如何提供這部分庫的高效實現了。
2、Android系統上的視訊框架
multimedia framework 架構 由三大部分構成:供上層程式呼叫的java API,連線java和C/C++的jni部分,多媒體引擎(stagefright)和codec介面(openmax interface)。
我們可以發現上層APK要播放視訊,首先得獲得一個player,而這個player的型別根據你媒體檔案的型別來決定的,分配的任務由mediaplayerservice來完成,除了獲得player外最主要的是到底選用哪種編碼器進行編解碼,這個過程由awesomeplayer和omxcodec來完成,至於聲音和影象就交由audioflinger和surfaceflinger來完成了。
android中播放視訊的兩種方式
通過VideoView配合MediaPlayerControl實現
概念介紹
VideoView是android系統提供的一個媒體播放顯示和控制的控制元件。其結構層次如下:
1 | VideoView extends SurfaceView implements MediaController.MediaPlayerControl |
通過VideoView的原型可知:如果構建更為複雜和有特色個性的視訊View,需要繼承SurfaceView 和實現MediaPlayerControl介面。其中SurfaceView 為顯示提供支援,MediaPlayerControl則為媒體控制提供了支援
基本方法介紹
VideoView:用於播放一段視訊媒體,它繼承了SurfaceView,位於”android.widget.VideoView”,是一個視訊控制元件。既然是播放一段視訊,那麼不可避免的要涉及到一些開始、暫停、停止等操作,VideoView也為開發人員提供了對應的方法,這裡簡單介紹一些常用的:
1234567891011121314 | int getCurrentPosition():獲取當前播放的位置。 int getDuration():獲取當前播放視訊的總長度。 isPlaying():當前VideoView是否在播放視訊。 void pause():暫停 void seekTo(int msec):從第幾毫秒開始播放。 void resume():重新播放。 void setVideoPath(String path):以檔案路徑的方式設定VideoView播放的視訊源。 void setVideoURI(Uri uri):以Uri的方式設定VideoView播放的視訊源,可以是網路Uri或本地Uri。 void start():開始播放。 void stopPlayback():停止播放。 setMediaController(MediaController controller):設定MediaController控制器。 setOnCompletionListener(MediaPlayer.onCompletionListener l):監聽播放完成的事件。 setOnErrorListener(MediaPlayer.OnErrorListener l):監聽播放發生錯誤時候的事件。 setOnPreparedListener(MediaPlayer.OnPreparedListener l)::監聽視訊裝載完成的事件。 |
上面的一些方法通過方法名就可以瞭解用途。和MediaPlayer配合SurfaceView播放視訊不同,VideoView播放之前無需編碼裝載視訊,它會在start()開始播放的時候自動裝載視訊。並且VideoView在使用完之後,無需編碼回收資源。
基本使用
要使用 VideoView類播放視訊,首先要在佈局檔案中新增VideoView元件,然後在Activity中獲取該元件,並使用 VideoView.setVideoPath()或VideoView.setVideoURI() 方法載入需要播放的視訊,最後呼叫 start()方法播放視訊。VideoView類還提供了stop()和pause()方法,用於停止或暫停視訊播放。
程式碼示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748 | package com.example.leeyou.drr.di;import java.io.File;import android.app.Activity;import android.media.MediaPlayer;import android.media.MediaPlayer.OnCompletionListener;import android.os.Bundle;import android.widget.MediaController;import android.widget.Toast;import android.widget.VideoView;public class MainActivity extends Activity { private VideoView video; /** * Called when the activity is firstcreated. */ publicvoidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); video = (VideoView) findViewById(R.id.video); File file = new File("/sdcard/1.mp4"); MediaController mc = newMediaController(MainActivity.this); // 建立一個MediaController物件 if (file.exists()) { video.setVideoPath(file.getAbsolutePath()); video.setMediaController(mc); // 將VideoView與 MediaController關聯起來 video.requestFocus(); // 設定VideoView獲取焦點 try { video.start(); // 播放視訊 } catch (Exception e) { e.printStackTrace(); } // 設定 VideoView的Completion事件監聽器 video.setOnCompletionListener( new OnCompletionListener() { public voidonCompletion(MediaPlayer mp) { Toast.makeText(MainActivity.this, "視訊播放完畢!", Toast.LENGTH_SHORT).show(); } }); } else { Toast.makeText (this , "要播放的視訊檔案不存在 ", Toast.LENGTH_SHORT ).show(); } }} |
通過MediaPlay配合SurfaceView實現
基本使用
1、在佈局檔案中插入SurfaceView元件,其語法格式如下:
123456 | <SurfaceView android:id=”@+id/ID號” android:background=”背景” android:keepScreenOn=”true|false” android:layout_width=”寬度” android:layout_height=”高度” /> |
2、建立MediaPlayer物件,並載入要播放的視訊。載入視訊的方法和上一篇文章中介紹的載入音訊的方法一樣,這裡不再詳述。
3、將視訊畫面輸出到SurfaceView,語法格式如下:
MediaPlayer.setDisplay(SurfaceHolder sh)
引數sh用於指定SurfaceHolder物件,可以通過SurfaceView.getHolder()方法獲得。SurfaceHolder可以理解為SurfaceView裝載需要顯示的一幀幀影象的容器
4、呼叫MediaPlayer的play()、stop()、pause()等方法控制視訊播放。
程式碼示例
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 | package com.example.leeyou.drr.di;import java.io.IOException;import android.app.Activity;import android.media.MediaPlayer;import android.media.MediaPlayer.OnCompletionListener;import android.os.Bundle;import android.view.SurfaceView;import android.view.View;import android.view.View.OnClickListener;import android.widget.Button;import android.widget.Toast;public class MainActivity extends Activity { private MediaPlayer mp; private SurfaceView sv; publicvoidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mp = new MediaPlayer(); sv = (SurfaceView) findViewById(R.id.surfaceView1); Button play = (Button) findViewById(R.id.play); final Button pause = (Button) findViewById(R.id.pause); Button stop = (Button) findViewById(R.id.stop); play.setOnClickListener(new OnClickListener() { public void onClick(View v) { mp.reset(); try { mp.setDataSource( "/sdcard/1.mp4"); mp.setDisplay( sv.getHolder()); mp.prepare(); mp.start(); pause.setText( "暫停"); pause.setEnabled( true); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (IllegalStateException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }); stop.setOnClickListener(new OnClickListener() { public void onClick(View v) { if (mp.isPlaying()) { mp.stop(); pause.setEnabled( false); } } }); pause.setOnClickListener(new OnClickListener() { public void onClick(View v) { if (mp.isPlaying()) { mp.pause(); ((Button) v).setText( "繼續"); } else { mp.start(); ((Button) v).setText( "暫停"); } } }); mp.setOnCompletionListener(new OnCompletionListener() { public voidonCompletion(MediaPlayer mp) { Toast.makeText(MainActivity.this, "視訊播放完畢!", Toast.LENGTH_SHORT ).show(); } }); } protected void onDestroy() { if (mp.isPlaying()) { mp.stop(); } mp.release(); super.onDestroy(); }} |
SurfaceView介紹
基本介紹
先來介紹一下大部分軟體如何解析一段視訊流。首先它需要先確定視訊的格式,這個和解碼相關,不同的格式視訊編碼不同,不是這裡的重點。知道了視訊的編碼格式後,再通過編碼格式進行解碼,最後得到一幀一幀的影象,並把這些影象快速的顯示在介面上,即為播放一段視訊。SurfaceView在Android中就是完成這個功能的。
既然SurfaceView是配合MediaPlayer使用的,MediaPlayer也提供了相應的方法設定SurfaceView顯示圖片,只需要為MediaPlayer指定SurfaceView顯示影象即可。它的完整簽名如下:
1 | void setDisplay(SurfaceHolder sh) |
它需要傳遞一個SurfaceHolder物件,SurfaceHolder可以理解為SurfaceView裝載需要顯示的一幀幀影象的容器,它可以通過SurfaceHolder.getHolder()方法獲得。
使用MediaPlayer配合SurfaceView播放視訊的步驟與播放使用MediaPlayer播放MP3大體一致,只需要額外設定顯示的SurfaceView即可。
緩衝原理介紹
上面有提到,SurfaceView和大部分視訊應用一樣,把視訊流解析成一幀幀的影象進行顯示,但是如果把這個解析的過程放到一個執行緒中完成,可能在上一幀影象已經顯示過後,下一幀影象還沒有來得及解析,這樣會導致畫面的不流暢或者聲音和視訊不同步的問題。
所以SurfaceView和大部分視訊應用一樣,通過雙緩衝的機制來顯示幀影象。
那麼什麼是雙緩衝呢?雙緩衝可以理解為有兩個執行緒輪番去解析視訊流的幀影象,當一個執行緒解析完幀影象後,把影象渲染到介面中,同時另一執行緒開始解析下一幀影象,使得兩個執行緒輪番配合去解析視訊流,以達到流暢播放的效果。
SurfaceHolder
SurfaceView內部實現了雙緩衝的機制,但是實現這個功能是非常消耗系統記憶體的。因為移動裝置的侷限性,Android在設計的時候規定,SurfaceView如果為使用者可見的時候,建立SurfaceView的SurfaceHolder用於顯示視訊流解析的幀圖片,如發現SurfaceView變為使用者不可見的時候,則立即銷燬SurfaceView的SurfaceHolder,以達到節約系統資源的目的。
如果開發人員不對SurfaceHolder進行維護,會出現最小化程式後,再開啟應用的時候,視訊的聲音在繼續播放,但是不顯示畫面了的情況,這就是因為當SurfaceView不被使用者可見的時候,之前的SurfaceHolder已經被銷燬了,再次進入的時候,介面上的SurfaceHolder已經是新的SurfaceHolder了。
所以SurfaceHolder需要我們開發人員去編碼維護,維護SurfaceHolder需要用到它的一個回撥,SurfaceHolder.Callback(),它需要實現三個如下三個方法:
123 | void surfaceDestroyed(SurfaceHolder holder):當SurfaceHolder被銷燬的時候回撥。void surfaceCreated(SurfaceHolder holder):當SurfaceHolder被建立的時候回撥。void surfaceChange(SurfaceHolder holder):當SurfaceHolder的尺寸發生變化的時候被回撥。 |
以下是這三個方法的呼叫過程,在應用中分別為SurfaceHolder實現了這三個方法,先進入應用,SurfaceHolder被建立,建立好之後會改變SurfaceHolder的大小,然後按Home鍵回退到桌面銷燬SurfaceHolder,最後再進入應用,重新SurfaceHolder並改變其大小。
擴充套件
MediaStore
MediaStore這個類是android系統提供的一個多媒體資料庫,android中多媒體資訊都可以從這裡提取。這個MediaStore包括了多媒體資料庫的所有資訊,包括音訊,視訊和影象,android把所有的多媒體資料庫介面進行了封裝,所有的資料庫不用自己進行建立,直接呼叫利用ContentResolver去呼叫那些封裝好的介面就可以進行資料庫的操作,多媒體資料庫的使用方法和SQLITE3的方法是一樣的。
MediaStore中的資料是在MediaScanner掃描後通過MediaProvider中的一個service進行更新的。
MediaScanner
在Android系統中,多媒體庫是通過MediaScanner去掃描磁碟檔案,對元資訊的處理,並通過MediaProvider儲存到MediaStore中。
MediaScanner可以通過手動控制,在ANDROID系統中,已經定製了三種事件會觸發MediaScanner去掃描磁碟檔案:
ACTION_BOOT_COMPLETED【系統啟動完後發出這個訊息】
ACTION_MEDIA_MOUNTED 【插卡事件觸發的訊息】
ACTION_MEDIA_SCANNER_SCAN_FILE【一般是在一些檔案操作後,開發人員手動發出的一個重新掃描多媒體檔案的訊息】
傳送訊息通過sendBroadcast函式完成,比如廣播一個ACTION_MEDIA_MOUNTED訊息:
1 | sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory()))); |
視訊編解碼
微校中採用的是FFmpeg開源框架進行視訊的解碼處理。FFmpeg在Linux平臺下開發,但它同樣也可以在其它作業系統環境中編譯執行,包括Windows、Mac OS X等。
FFmpeg專案的名稱來自MPEG視訊編碼標準,前面的”FF”代表”Fast Forward”。
其實電影檔案有很多基本的組成部分。首先,檔案本身被稱為容器Container,容器的型別決定了檔案資訊存放的位置。接著,你有一組流,例如,你經常有的是一個音訊流和一個視訊流。在流中的資料元素被稱為幀Frame。每個流是由不同的編碼器來編碼生成的。接著從流中被讀出來的叫做包Packets。包是一段資料,它包含了一段可以被解碼成方便我們最後在應用程式中操作的原始幀的資料。
我們可以利用GraphEdit工具對視訊檔案進行過濾,看到其中的解碼思路。大致分為七個模組分別為:讀檔案模組,解複用模組,視訊解碼模組,音訊解碼音訊,顏色空間轉換模組,視訊顯示模組(顯示卡),音訊播放模組(音效卡)。
對應具體的程式碼實現,粗略的分為五類,分別是Source filter, Demux flter, Decoder filter, Color Space converter filter,Render filter
Source filter 源過濾器的作用是為下級 demux filter 以包的形式源源不斷的提供資料流。Sourcefilter 另外一個作用就是遮蔽讀本地檔案和獲取網路資料的差別,在下一級的 demux filter 看來,本地檔案和網路資料是一樣的。
Demux flter解複用過濾器的作用是識別檔案型別,媒體型別,分離出各媒體原始資料流,打上時鐘資訊後送給下級 decoder filter。在本例中,AVI Splitter 是 Demux filter。
Decoder filter解碼過濾器的作用就是解碼資料包,並且把同步時鐘資訊傳遞下去。對視訊媒體而言,通常是解碼成 YUV 資料,然後利用顯示卡直接支援 YUV 格式資料 Overlay 快速顯示的特性讓顯示卡極速顯示。對音訊媒體而言,通常是解碼成 PCM 資料,然後送給音效卡直接輸出。在本例中,AVI Decompress 和 ACM Warper 是 decoder filter。
Color Space converter filter顏色空間轉換過濾器的作用是把視訊解碼器解碼出來的資料轉換成當前顯示系統支援的顏色格式。通常視訊解碼器解碼出來的是 YUV 資料,PC 系統是直接支援 YUV 格式的,也支援 RGB 格式,有些嵌入式系統只支援 RGB 格式的。在本例中,視訊解碼器解碼出來的是 RGB8 格式的資料,Color space converter filter 把 RGB8 轉換成 RGB32 顯示。
Render filter渲染過濾器的作用就是在適當的時間渲染相應的媒體,對視訊媒體就是直接顯示影象,對音訊就是播放聲音。視訊必須要使用同步時鐘資訊來決定什麼時候顯示,而音訊是可以基於視訊的時鐘資訊通過計算進行播放。在本例中 VideoRender 和 Default DirectSound Device 是 Render filter,同時也是 Sink filter。
HLS(HTTP Live Streaming)視訊直播技術
常用的流媒體協議主要有 HTTP 漸進下載和基於 RTSP/RTP 的實時流媒體協議,這二種基本是完全不同的東西,目前比較方便又好用的是 HTTP 漸進下載的方法。
處理步驟:
視訊採集 ->編碼器 -> 流分割 -> 普通 web 服務(索引檔案和視訊檔案) -> 客戶端
RTSP(Real Time Streaming Protocol),實時流傳輸協議,是TCP/IP協議體系中的一個應用層協議,由哥倫比亞大學、網景和RealNetworks公司提交的IETF RFC標準。該協議定義了一對多應用程式如何有效地通過IP網路傳送多媒體資料。RTSP在體系結構上位於RTP和RTCP之上,它使用TCP或RTP完成資料傳輸。
HTTP與RTSP相比,HTTP傳送HTML,而RTSP傳送的是多媒體資料。HTTP請求由客戶機發出,伺服器作出響應;使用RTSP時,客戶機和伺服器都可以發出請求,即RTSP可以是雙向的。
RTP(Real-time Transport Protocol,實時傳輸協議)是一個網路傳輸協議,它是由IETF的多媒體傳輸工作小組1996年在RFC 1889中公佈的,後在RFC3550中進行更新。
對於視訊採集,編碼器首先將攝像機實時採集的音視訊資料壓縮編碼為符合特定標準的音視訊基本流,也可以用編碼完了的檔案,有一點必須保證,就是一定要使用H.264視訊和AAC音訊,因為發明這個的是蘋果公司,只支援這個。然後給這些封裝成為符合MPEG-2(MPEG 2 TS、MPEG2 PS之所以使用這個,主要是因為聲音和視訊會交織在一起,也會有關鍵幀來讓視訊可以直接播放).
流分割部分比起 RTSP 之類和普通點播的最大不同,就是他會給 MPEG-2 分割成很多個 ts 的檔案。分割過程大多是按時間來切,建議切 10s 一個的檔案,如果碼流高可以 5 s一次。在分割還有一點不同,就是這時流分割器會生成一個含有指向這些小ts檔案指標的索引檔案.
所以這個檔案也必須在 web 伺服器上,不能少。每多 10s 時,就會多一個 ts 檔案,所以索引也會跟著修改成最新的幾段視訊。
最後,這些切分了的小的一系列的 ts 檔案,放到普通的 web 伺服器中就行了。因為請求這些檔案會使用標準的 HTTP 協議。索引檔案字尾是.m3u8 , 索引檔案採用擴充套件的M3U播放列表格式, 其實就一文字。
最後就是客戶端,如果是 HTML 直接在 HTML5 中直接支援這種視訊可以使用如下標籤
1 | <video tabindex="0" height="480" width="640"> <source src="/content1/content1.m3u8"> </video> |
如果是應用客戶端(Safari QuickTime之類),就得裝軟體來支援,客戶端會根據選擇的流的索引來下載檔案,當下載了最少二段後開始播放。直接 m3u8 的索引結束。
另外,HTTP可以設計成的自適應位元率流,在不同網路環境,選擇下載不同碼流的視訊。
所以整個 HTTP Live Streaming 無論是直播還是點播,都能做到近似實時的方式來進行流播放。理論的最小時延是每個切片的長.