Android車載應用開發與分析(6)- 車載多媒體(一)- 音視頻基礎知識與MediaPlayer

多媒體應用是車載信息娛樂系統的一個重要組成部分,一般包含音視頻播放、收音機、相冊等。車載應用多媒體系列初步計劃分為六篇,這是第一篇。

參考資料
視頻和視頻幀:視頻和幀基礎知識整理
百度百科 - 聲道百度百科 - 量化精度
管理音頻焦點 | Android 開發者 | Android Developers
Android音視頻開發 - 何俊林
MediaPlayer | Android Developers
MediaPlayer 概覽 | Android 開發者 | Android Developers

1. 音視頻基礎知識

1.1 視頻編碼

視頻編碼就是指通過特定的壓縮技術,將某個視頻格式文件轉換成另一種視頻格式文件的方式。
視頻編碼分為以下兩個系列:
MPEG系列:由ISO[國際標準化組織]下屬的MPEG[動態影像專家組]開發。視頻編碼方面主要是MPEG1(VCD使用)、MPEG2(DVD使用)、MPEG4(DVD RIP使用的都是它的變種,如DivX、XviD等)、MPEG4 AVC(目前最常用)。其還有音頻編碼方面,主要有MPEG Audio Layer1/2、MPEG Audio Layer 3(MP3)、MPEG-2 AAC、MPEG4-AAC等。DVD音頻沒有采用MPEG。
H.26X系列:由ITU[國際電傳視訊聯盟]主導,側重網絡傳輸,但只有視頻編碼。包括H.261、H.262、H.263、H.263+、H.263++、H.264(與MPEG4 AVC合作的產物)。

1.2 音頻編碼

常見的音頻編碼格式有AAC、MP3、AC3。
AAC:AAC,全稱Advanced Audio Coding,是一種專為聲音數據設計的文件壓縮格式。與MP3不同,它采用了全新的算法進行編碼,更加高效,具有更高的“性價比”。利用AAC格式,可使人感覺聲音質量沒有明顯降低的前提下,更加小巧。蘋果ipod、諾基亞手機支持AAC格式的音頻文件。優點:相較于mp3,AAC格式的音質更佳,文件更小。不足:AAC屬于有損壓縮的格式,與時下流行的APE、FLAC等無損格式相比音質存在“本質上”的差距。加之,傳輸速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC頭上“小巧”的光環不復存在。
MP3:MP3是一種音頻壓縮技術,其全稱是動態影像專家壓縮標準音頻層面3(Moving Picture Experts Group Audio Layer III),簡稱為MP3。它被設計用來大幅度地降低音頻數據量。利用 MPEG Audio Layer 3 的技術,將音樂以1:10 甚至 1:12 的壓縮率,壓縮成容量較小的文件,而對于大多數用戶來說重放的音質與最初的不壓縮音頻相比沒有明顯的下降。
MP3是利用人耳對高頻聲音信號不敏感的特性,將時域波形信號轉換成頻域信號,并劃分成多個頻段,對不同的頻段使用不同的壓縮率,對高頻加大壓縮比(甚至忽略信號)對低頻信號使用小壓縮比,保證信號不失真。這樣一來就相當于拋棄人耳基本聽不到的高頻聲音, [1] 只保留能聽到的低頻部分,從而將聲音用1∶10甚至1∶12的壓縮率壓縮。由于這種壓縮方式的全稱叫MPEG Audio Player3,所以人們把它簡稱為MP3。
根據MPEG規范的說法,MPEG-4中的AAC(Advanced audio coding)將是MP3格式的下一代。
最高參數的MP3(320Kbps)的音質較之CD的,FLAC和APE無損壓縮格式的差別不多,其優點是壓縮后占用空間小,適用于移動設備的存儲和使用。
AC3:全稱Audio Coding3(音頻編碼3)是杜比數碼(Dolby Digital)的同義詞,杜比數碼是一種高級音頻壓縮技術,它最多可以對6個比特率最高為448kbps的單獨聲道進行編碼。杜比AC-3提供的環繞聲系統由5個全頻域聲道和1個超低音聲道組成,被稱為5.1聲道。5個聲道包括左前、中央、右前、左后、右后。低音聲道主要提供一些額外的低音信息,使一些場景,如爆炸、撞擊等聲音效果更好。

1.3 多媒體播放組件

在Android系統中多媒體播放組件包含MediaPlayer、MediaCodec、OMX、StageFright、AudioTrack 。

  • MediaPlayer:將系統提供的解碼器封裝后提供給應用開發者使用音視頻播放組件,一般支持多種多媒體格式。
  • MediaCodec:音視頻編解碼 。
  • OMX:多媒體部分采用的編解碼標準。
  • Stagefright:Stagefright是位于原生層的媒體播放引擎,內置了基于軟件的編解碼器,可用于處理熱門媒體格式。它是用來替代之前OpenCore框架,主要做了一層OMX層,僅僅對OpenCore的omx-component部分做了引用。Stagefright是在MediaPlayerService這一層加入的,和OpenCore是并列的。StageFright在Android中以共享庫的形式存在(libstagefright.so),其中的Module - NuPlayer/AwesomePlayer可用來播放音視頻。NuPlayer/AwesomePlayer提供了許多的API,可以讓上層的應用程序(Java/JNI)調用。
  • AudioTrack:音頻播放組件,與MediaPlayer不同的是,AudioTrack僅支持非壓縮編碼(PCM)的音頻。

1.4 音視頻中的專業術語

1.4.1 幀率

幀率(frame rate)是用于測量顯示幀數的度量。測量單位為“每秒顯示幀數”(frame per secondFPS)或“赫茲”,一般來說FPS用于描述視頻、電子繪圖或游戲每秒播放多少幀。
每秒的幀數(fps)或者說幀率表示圖形處理器處理場時每秒鐘能夠更新的次數。高的幀率可以得到更流暢、更逼真的動畫。一般來說30fps就是可以接受的,但是將性能提升至60fps則可以明顯提升交互感和逼真感,但是一般來說超過75fps一般就不容易察覺到有明顯的流暢度提升了。如果幀率超過屏幕刷新率只會浪費圖形處理的能力,因為監視器不能以這么快的速度更新,這樣超過刷新率的幀率就浪費掉了。

1.4.2 分辨率

視頻分辨率,是用于度量圖像內數據量多少的一個參數,通常表示成ppi。
視頻的320X180是指它在橫向和縱向上的有效像素,窗口小時ppi值較高,看起來清晰;窗口放大時,由于沒有那么多有效像素填充窗口,有效像素ppi值下降,就模糊了。

1.4.3 刷新率

刷新率是指電子束對屏幕上的圖像重復掃描的次數。刷新率越高,所顯示的圖像(畫面)穩定性就越好。
刷新率分為垂直刷新率和水平刷新率,一般提到的刷新率通常指垂直刷新率。垂直刷新率表示屏幕的圖像每秒鐘重繪多少次,也就是每秒鐘屏幕刷新的次數,以Hz(赫茲)為單位。刷新率越高越好,圖像就越穩定,圖像顯示就越自然清晰,對眼睛的影響也越小。刷新頻率越低,圖像閃爍和抖動的就越厲害,眼睛疲勞得就越快。一般來說,如能達到80Hz以上的刷新頻率就可完全消除圖像閃爍和抖動感,眼睛也不會太容易疲勞。

1.4.4 編碼格式

編碼的目的就是指通過特定的壓縮技術,將某個視頻格式的文件轉換成另一種視頻格式文件的方式,主要目標是壓縮數據量。常用的編碼格式有MPEG和H.26X兩種。

1.4.5 封裝格式

封裝格式(也叫容器),就是將已經編碼壓縮好的視頻軌和音頻軌按照一定的格式放到一個文件中,也就是說僅僅是一個外殼。常見的封裝格式有:
AVI:微軟在90年代初創立的封裝標準,是當時為對抗quicktime格式(mov)而推出的,只能支持固定CBR恒定比特率編碼的聲音文件。
FLV:針對于h.263家族的格式。
MKV:萬能封裝器,有良好的兼容和跨平臺性、糾錯性,可帶 外掛字幕。
MOV:MOV是Quicktime封裝。
MP4:主要應用于mpeg4的封裝 。
RM/RMVB:Real Video,由RealNetworks開發的應用于rmvb和rm 。
TS/PS:PS封裝只能在HDDVD原版。
WMV:微軟推出的,作為市場競爭。

1.4.6 碼率

比特率又稱“二進制位速率”,俗稱“碼率”。表示單位時間內傳送比特的數目。用于衡量數字信息的傳送速度,常寫作bit/sec。根據每幀圖像存儲時所占的比特數和傳輸比特率,可以計算數字圖像信息傳輸的速度。碼率越高,消耗的帶寬越多。
文件大小(b) = 碼率(b/s) X 時長(s)

1.4.7 畫質&碼率

通常我們有一個錯誤的認識,碼率越大畫質越好!實際上視頻質量和碼率、編碼算法都有關系。

1.4.8 DTS&PTS

DTS:解碼時間標簽(Decoding Time Stamp)。主要用于標識讀入內存中的比特流在什么時候開始送入解碼器中進行解碼。
PTS:演示時間標簽(Presentation Time Stamp)。主要用于度量解碼后的視頻幀什么時候被顯示出來。

1.4.9 YUV&RGB

YUV:是一種顏色編碼方法。YUV是編譯true-color顏色空間(color space)的種類,Y'UV, YUV, YCbCr,YPbPr等專有名詞都可以稱為YUV,彼此有重疊。“Y”表示明亮度(Luminance或Luma),也就是灰階值,“U”和“V”表示的則是色度(Chrominance或Chroma),作用是描述影像色彩及飽和度,用于指定像素的顏色。
RGB:RGB色彩模式是工業界的一種顏色標準,是通過對紅(R)、綠(G)、藍(B)三個顏色通道的變化以及它們相互之間的疊加來得到各式各樣的顏色的,RGB即是代表紅、綠、藍三個通道的顏色,這個標準幾乎包括了人類視力所能感知的所有顏色,是運用最廣的顏色系統之一。

1.4.10 視頻幀&音頻幀

常見的視頻幀有I、P、B幀等。

  • I幀Intra Picture,又稱幀內編碼幀,俗稱關鍵幀。一般來說I幀不需要依賴前后幀信息,可獨立進行解碼。
  • P幀predictive-frame,又稱前向預測編碼幀,也有幀間預測編碼幀。顧名思義,P幀需要依賴前面的I幀或者P幀才能進行編解碼,因為一般來說,P幀存儲的是當前幀畫面與前一幀(前一幀可能是I幀也可能是P幀)的差別,較專業的說法是壓縮了時間冗余信息,或者說提取了運動特性P幀的壓縮率約在20左右,幾乎所有的H264編碼流都帶有大量的P幀
  • B幀bi-directional interpolatedprediction frame,又稱雙向預測內插編碼幀,簡稱雙向預測編碼幀B幀非常特殊,它存儲的是本幀與前后幀的差別,因此帶有B幀的視頻在解碼時的邏輯會更復雜些,CPU開銷會更大。因此,不是所有的視頻都帶有B幀,不過,B幀的壓縮率能夠達到50甚至更高,在壓縮率指標上還是很可觀的。

音頻幀的概念沒有視頻幀那么清晰,音頻幀與編碼格式有關:

  • 對于PCM(脈沖編碼調制,非壓縮編碼)來說,它不需要幀的概念,根據采樣率和采樣精度就可以播放。
  • AMR幀比較簡單,它規定每20ms的音頻就是1幀,每1幀的音頻都是獨立的,有可能采用不同的編碼算法以及不同的編碼參數。
  • MP3幀較為復雜一些,包含了更多信息,比如采樣率、比特率等各種參數。具體如下:音頻數據幀個數由文件大小和幀長決定,每一幀的長度可能不固定,也可能固定,由比特率決定,每一幀又分為幀頭和數據實體兩部分,幀頭記錄了MP3的比特率、采樣率、版本等信息,每一幀之間相互獨立。

1.4.11 量化精度

量化精度,是指可以將模擬信號分成多少個等級,量化精度越高,越接近原波形。

1.4.12 采樣率

采樣頻率,也稱為采樣速度或者采樣率,定義了單位時間內從連續信號中提取并組成離散信號的采樣個數,它用赫茲(Hz)來表示。采樣頻率的倒數是采樣周期或者叫做采樣時間,它是采樣之間的時間間隔。通俗的講采樣頻率是指計算機單位時間內能夠采集多少個信號樣本。

1.4.13 聲道

聲道(Sound Channel) 是指聲音在錄制或播放時在不同空間位置采集或回放的相互獨立的音頻信號,所以聲道數也就是聲音錄制時的音源數量或回放時相應的揚聲器數量。
立體聲道:單聲道缺乏對聲音的位置定位,而立體聲技術則徹底改變了這一狀況。聲音在錄制過程中被分配到兩個獨立的聲道,從而達到了很好的聲音定位效果。這種技術在音樂欣賞中顯得尤為有用,聽眾可以分辨出各種樂器來自的方向,從而使音樂更富想象力,更加接近于臨場感受。立體聲技術廣泛運用于自Sound Blaster Pro以后的大量聲卡,成為了影響深遠的一個音頻標準。時至今日,立體聲依然是許多產品遵循的技術標準。
4聲道:四聲道環繞規定了4個發音點:前左、前右,后左、后右,聽眾則被包圍在這中間。同時還建議增加一個低音音箱,以加強對低頻信號的回放處理(這也就是如今4.1聲道音箱系統廣泛流行的原因)。就整體效果而言,四聲道系統可以為聽眾帶來來自多個不同方向的聲音環繞,可以獲得身臨各種不同環境的聽覺感受,給用戶以全新的體驗。如今四聲道技術已經廣泛融入于各類中高檔聲卡的設計中,成為未來發展的主流趨勢。
5.1聲道:5.1聲道已廣泛運用于各類傳統影院和家庭影院中,一些比較知名的聲音錄制壓縮格式,譬如杜比AC-3(Dolby Digital)、DTS等都是以5.1聲音系統為技術藍本的,其中“.1”聲道,則是一個專門設計的超低音聲道,這一聲道可以產生頻響范圍20~120Hz的超低音。其實5.1聲音系統來源于4.1環繞,不同之處在于它增加了一個中置單元。這個中置單元負責傳送低于80Hz的聲音信號,在欣賞影片時有利于加強人聲,把對話集中在整個聲場的中部,以增加整體效果。相信每一個真正體驗過Dolby AC-3音效的朋友都會為5.1聲道所折服。


7.1聲道:7.1聲道系統的作用簡單來說就是在聽者的周圍建立起一套前后聲場相對平衡的聲場,不同于5.1聲道聲場的是,它在原有的基礎上增加了后中聲場聲道,同時它也不同于普通6.1聲道聲場,因為 [2] 7.1聲道有雙路后中置,而這雙路后中置的最大作用就是為了防止聽者因為沒有坐在皇帝位而在聽覺上產生聲場的偏差。

2.系統播放器 - MediaPlayer

MediaPlayer是Android系統中的一個多媒體播放類,通過它能控制音視頻流或本地音視頻資源的播放過程。在車載音視頻應用開發中,許多時候我們會直接采用MediaPlayer,當然可以使用ExoPlayer,這個挖個坑以后再講。

2.1 MediaPlayer的狀態與生命周期

音頻/視頻文件和流的播放控制作為狀態機進行管理。下圖顯示了由支持的播放控制操作驅動的 MediaPlayer 對象的生命周期和狀態(這張圖很重要,使用MediaPlayer開發音視頻應用時,務必參考)。
橢圓形表示 MediaPlayer 對象可能駐留的狀態。弧表示驅動對象狀態過渡的播放控制操作。有兩種類型的弧。具有單個箭頭的弧表示同步方法調用,而具有雙箭頭的弧表示異步方法調用。


從此狀態圖中,可以看到 MediaPlayer 對象具有以下狀態:

  • Idel 和 End 狀態
    當創建MediaPlayer實例或調用reset()后,它處于Idle(空閑/就緒)狀態;調用release()之后,它處于End(結束)狀態。在Idle和End之間的狀態就是**MediaPlayer**對象的生命周期
  • Error 狀態
    在創建一個新的MediaPlayer的實例或調用reset()后,此時MediaPlayer尚處于Idle狀態,如果此時調用getCurrentPosition()getDuration()getVideoHeight()getVideoWidth()setAudioAttributes(android.media.AudioAttributes)setLooping(boolean)setVolume(float, float)pause()start()stop()seekTo(long, int)prepare()prepareAsync()程序就會出錯,并觸發setOnErrorListener設定的OnErrorListener.onError,然后MediaPlayer的生命周期就會進入Error狀態。
    通常,某些播放控制操作可能會由于各種原因而失敗,例如音頻/視頻格式不受支持、音頻/視頻交錯不良、分辨率過高、流超時等。因此,在這些情況下,錯誤報告和恢復是一個重要的問題。有時,由于編程錯誤,還可能發生在無效狀態下調用播放控制操作的情況。在所有這些錯誤條件下,都會觸發setOnErrorListener設定的OnErrorListener.onErrorMediaPlayer的生命周期同樣會進入Error狀態。
    為了重新使用MediaPlayer,調用reset()可以將MediaPlayer恢復到Idle狀態
  • End 狀態
    MediaPlayer會占用寶貴的系統資源。因此,應該始終采取額外的預防措施,確保 MediaPlayer對象保留的時間不會過長。調用 release()可以確保分配給MediaPlayer的系統資源得到釋放。此時MediaPlayer的生命周期則會進入End(結束)狀態。
    MediaPlayer處于End狀態時,它就不能再使用了,也無法再進入其它的生命狀態
  • Initialized 狀態
    處于Idle狀態時,調用setDataSource(java.io.FileDescriptor)setDataSource(java.lang.String)setDataSource(android.content.Context, android.net.Uri)setDataSource(java.io.FileDescriptor, long, long)setDataSource(android.media.MediaDataSource)中的任意方法,MediaPlayer會進入Initialized(已初始化)狀態。
    如果在非Idle狀態調用setDataSource(),會引發IllegalStateException。
    重載setDataSource(),需要拋出IllegalArgumentExceptionIOException
  • Prepared 狀態
    MediaPlayer對象必須先進入Prepared(已準備)狀態,然后才能開始播放。
    有兩種途徑到達Prepared狀態。一種是調用prepare(),這是一種同步方法,由于prepare本身是耗時操作,雖然有時候它執行的很快,但也不要在主線程執行它,否則可能導致ANR。另一種方式是調用prepareAsync(),它是一種異步方法,可以在主線程中執行。調用prepareAsync()并不會立即進入Prepared狀態,而是先進入Preparing狀態,最后到達Prepared狀態。需要注意Preparing是一種瞬間狀態,存在時間很短通過注冊setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)可以監聽MediaPlayerPrepare狀態。
    Prepared狀態下,VolumescreenOnWhilePlayingLooping等屬性已經可以通過調用相應的set 方法來調整了。
    如果在非Initialized狀態調用prepare() / prepareAsync(),會引發IllegalStateException。
  • Started狀態
    在播放之前必須調用start()并成功返回,此時MediaPlayer狀態進入Started(已啟動)狀態,通過注冊setOnBufferingUpdateListener(android.media.MediaPlayer.OnBufferingUpdateListener)可以保持跟蹤音視頻流的buffering(緩沖)status。
    MediaPlayer處于Started狀態時,可以通過調用seekTo(long, int)來調整播放位置。盡管異步調用會立即返回,但實際的尋道操作可能需要一段時間才能完成,特別是對于正在流式傳輸的音頻/視頻。當實際的尋道操作完成時,如果事先注冊了setOnSeekCompleteListener(android.media.MediaPlayer.OnSeekCompleteListener),則內部播放器引擎會回調OnSeekComplete.onSeekComplete()。
    此外,可以通過調用getCurrentPosition()來檢索實際的當前播放位置,這對于需要跟蹤播放進度的應用程序(如 Music 播放器)非常有用。
  • Paused狀態
    在音視頻播放過程中調用pause()方法,MediaPlayer的狀態從Started狀態進入Paused(已暫停)狀態,這個過程是瞬間的且在播放器內部是異步的。反之,從Paused通過調用start()返回Started也是同樣的。在狀態更新并調用isPlaying()方法前,將有一些耗時,對流數據可能需要耗費數秒。
    MediaPlayer處于Paused,此時調用seekTo(long, int)來調整播放位置時,如果數據流具有視頻并且請求的位置有效,則將顯示一個視頻幀。
  • Stopped狀態
    當調用stop()方法時,MediaPlayer無論是處于StartedPausedPrepared還是PlaybackCompleted中哪種狀態,都將進入Stoped(已停止)狀態。一旦進入Stoped狀態,playback將不能開始,直到調用prepare() / prepareAsync(),且處于Prepared狀態才可以開始。
  • PlaybackCompleted狀態
    當MediaPlayer播放到數據流末尾時,一次播放完成。如果事先setLooping(true)MediaPlayer依然處于Started狀態,并重新開始播放。如果實現setLooping(false),如果事先注冊了setOnCompletionListener(android.media.MediaPlayer.OnCompletionListener),就會回調 OnCompletion.onCompletion(),表示MediaPlayer進入PlaybackCompleted狀態。處于PlaybackCompleted時調用start()方法會重啟播放器從頭開始播放數據,此時狀態進入Started。
    MediaPlayer處于PlaybackCompleted,此時調用seekTo(long, int)來調整播放位置時,如果數據流具有視頻并且請求的位置有效,則將顯示一個視頻幀。

2.2 MediaPlayer 簡略使用

2.2.1 權限聲明

在開始使用MediaPlayer開發應用之前,還需要在manifest中添加適當的聲明,這樣才能使用相關功能。

  • 互聯網權限 - 如果需要使用MediaPlayer播放基于網絡的內容,則必須申請網絡訪問權限。
<uses-permission android:name="android.permission.INTERNET" />
  • 喚醒鎖定權限 - 如果播放器應用需要防止屏幕變暗或處理器進入休眠狀態,或者要使用
    MediaPlayer.setScreenOnWhilePlaying()MediaPlayer.setWakeMode() 方法,則必須申請此權限。
<uses-permission android:name="android.permission.WAKE_LOCK" />

2.2.2 播放資源

MediaPlayer支持多種不同的媒體源,例如:

  • 本地資源(打包在應用中的資源)
  • 內部 URI,例如,從ContentProvider那獲取的 URI
  • 外部網址(流式傳輸)
    以下示例展示了如何播放作為本地原始資源(保存在應用的 res/raw/ 目錄中)提供的音頻:
var mediaPlayer: MediaPlayer? = MediaPlayer.create(context, R.raw.sound_file_1)
mediaPlayer?.start() // 不需要調用prepare,因為create中已經替我們做好了

在本例中,“原始”資源是指系統不會嘗試以任何特定方式解析的文件。不過,該資源的內容不應為原始音頻。它應該是采用某種支持的格式且經過適當編碼和格式化的媒體文件。
播放系統中本地可用的 URI(例如,可以通過ContentProvider獲取)中的內容方法如下:

val resUri: Uri = .... // 本地uri
val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
    setAudioStreamType(AudioManager.STREAM_MUSIC)
    setDataSource(applicationContext, resUri)
    prepare()
    start()
}

通過 HTTP 流式傳輸并播放遠程網址上的內容如下所示:

val url = "http://........" // 網絡url
val mediaPlayer: MediaPlayer? = MediaPlayer().apply {
    setAudioStreamType(AudioManager.STREAM_MUSIC)
    setDataSource(url)
    prepare() // 耗時操作,可能導致ANR
    start()
}

注意 :
如果是播放在線媒體文件,則該文件必須能夠進行漸進式下載。
使用 setDataSource() 時,必須捕獲或傳遞 IllegalArgumentException 和 IOException,因為引用的文件可能并不存在。

2.3 后臺播放

如果希望即使當應用未在屏幕上顯示時,應用仍會在后臺播放媒體內容,則必須啟動一個Service并由此控制 MediaPlayer 實例。

2.4 喚醒鎖定

當設計在后臺播放媒體內容的應用時,手機等移動設備可能會在Service運行時進入休眠狀態,車載設備則無法確定,目前尚無統一的規范,每個主機廠的策略可能都不相同。
在原生系統中通過調用setWakeMode()可以保持喚醒鎖定,完成該操作后,MediaPlayer 會在播放時保持指定的鎖定狀態,并在暫停或停止播放時釋放鎖定。

mediaPlayer = MediaPlayer().apply {
// 省略其它代碼
setWakeMode(applicationContext, PowerManager.PARTIAL_WAKE_LOCK)
}

不過,播放流媒體時,還需要保持wifi的鎖定狀態,否則可能出現wifi中斷的現象。

val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
val wifiLock: WifiManager.WifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock")
wifiLock.acquire()

當不再需要網絡時,通過如下方式釋放該鎖定。

wifiLock.release()

2.5 數字版權管理 (DRM)

從 Android 8.0(API 26)開始,MediaPlayer 包含支持播放受 DRM 保護的資料的 API。這些 API 與由 MediaDrm 提供的低級別 API 類似,但前者是在較高級別運行,并且不會公開底層提取器、DRM 和加密對象。
車載多媒體中出現DRM的情況可能不多(我還沒遇到過),如有需要可以參考Android官方的文檔:MediaPlayer 概覽 | Android 開發者 | Android Developers

3. 管理音頻焦點

以下內容選自管理音頻焦點 | Android 開發者 | Android Developers 有增刪。

兩個或兩個以上的 Android 應用可同時向同一輸出流播放音頻。系統會將所有音頻流混合在一起。雖然這是一項出色的技術,但卻會給用戶帶來很大的困擾。為了避免所有音樂應用同時播放,Android 引入了“音頻焦點”的概念。 一次只能有一個應用獲得音頻焦點。
所以當多媒體應用需要輸出音頻時,它需要請求音頻焦點,順利獲取后,才可以播放聲音。當其它應用請求音頻焦點時,Android系統會根據內部仲裁表中定義的優先級決定,該應用能否獲取音頻焦點。例如:在用戶使用藍牙電話應用時,該應用會獲取音頻焦點,收音機等音頻優先級較低的應用就會失去音頻焦點。

音頻焦點的管理在Android系統中不是強制的,即使應用失去音頻焦點,也是可以輸出音頻的。但是在車載多媒體應用開發時務必不能這么做,因為多數駕駛員會同時使用收音機和地圖導航,如果導航提示音被收音機的音頻壓制,就極有可能造成駕駛偏航甚至是交通事故!!

3.1 音頻焦點管理準則

多媒體應用一般會遵守以下的準則來管理音頻焦點:

  • 在即將開始播放之前調用 requestAudioFocus(),并驗證調用是否返回 AUDIOFOCUS_REQUEST_GRANTED
  • 在其他應用獲得音頻焦點時,停止或暫停播放,或降低音量。
  • 播放停止后,放棄音頻焦點。

3.2 Android 8.0 及更高版本中的音頻焦點

因為現有車載IVI系統絕大多數已經升級到Android 9.0甚至是更高的版本,所以Android 8.0之前的音頻焦點獲取方式,本文不再介紹。

從 Android 8.0(API 26)開始,調用requestAudioFocus()時,必須提供AudioFocusRequest參數。要釋放音頻焦點,請調用 abandonAudioFocusRequest() 方法,該方法也接受 AudioFocusRequest 作為參數。在請求和放棄焦點時,應使用相同的 AudioFocusRequest 實例。要創建 AudioFocusRequest,請使用 AudioFocusRequest.Builder。由于焦點請求始終必須指定請求的類型,因此此類型會包含在構建器的構造函數中。使用構建器的方法來設置請求的其他字段。
FocusGain 字段為必需字段;所有其他字段均為可選字段。

方法 備注
setFocusGain() 每個請求中都必須包含此字段。此字段的值與 Android 8.0 之前的 requestAudioFocus() 調用中所使用的 durationHint 值相同:AUDIOFOCUS_GAIN、AUDIOFOCUS_GAIN_TRANSIENT、AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 或 AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE。
setAudioAttributes() AudioAttributes 描述了應用的用例。系統會在應用獲得和失去音頻焦點時查看這些屬性。這些屬性取代了音頻流類型的概念。在 Android 8.0(API 26)及更高版本中,棄用了除音量控制以外的所有操作的音頻流類型。在焦點請求中使用與音頻播放器中相同的屬性(如此表下面的示例中所示)。首先使用 AudioAttributes.Builder 指定屬性,然后使用此方法將屬性分配給請求。如果未指定,則 AudioAttributes 默認為 AudioAttributes.USAGE_MEDIA。
setWillPauseWhenDucked() 當其他應用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 請求焦點時,持有焦點的應用通常不會收到 onAudioFocusChange() 回調,因為系統可以自行降低音量。如果需要暫停播放而不是降低音量,請調用 setWillPauseWhenDucked(true),然后創建并設置 OnAudioFocusChangeListener,具體如自動降低音量中所述。
setAcceptsDelayedFocusGain() 當焦點被其他應用鎖定時,對音頻焦點的請求可能會失敗。此方法可實現延遲獲取焦點,即在焦點可用時異步獲取焦點。請注意,要使“延遲獲取焦點”起作用,必須在音頻請求中指定 AudioManager.OnAudioFocusChangeListener,因為應用必須收到回調才能知道自己獲取了焦點。
setOnAudioFocusChangeListener() 只有在請求中還指定了 willPauseWhenDucked(true) 或 setAcceptsDelayedFocusGain(true) 時,才需要 OnAudioFocusChangeListener。有兩個方法可以設置監聽器:一個帶處理程序參數,一個不帶。處理程序是運行監聽器的線程。如果未指定處理程序,則會使用與主 Looper 關聯的處理程序。

以下示例展示了如何使用 AudioFocusRequest.Builder 構建 AudioFocusRequest 來請求和放棄音頻焦點

audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
    setAudioAttributes(AudioAttributes.Builder().run {
        setUsage(AudioAttributes.USAGE_GAME)
        setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
        build()
    })
    setAcceptsDelayedFocusGain(true)
    setOnAudioFocusChangeListener(afChangeListener, handler)
    build()
}
mediaPlayer = MediaPlayer()
val focusLock = Any()

var playbackDelayed = false
var playbackNowAuthorized = false

// ...
val res = audioManager.requestAudioFocus(focusRequest)
synchronized(focusLock) {
    playbackNowAuthorized = when (res) {
        AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false
        AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
            playbackNow()
            true
        }
        AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
            playbackDelayed = true
            false
        }
        else -> false
    }
}

// ...
override fun onAudioFocusChange(focusChange: Int) {
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN ->
            if (playbackDelayed || resumeOnFocusGain) {
                synchronized(focusLock) {
                    playbackDelayed = false
                    resumeOnFocusGain = false
                }
                playbackNow()
            }
        AudioManager.AUDIOFOCUS_LOSS -> {
            synchronized(focusLock) {
                resumeOnFocusGain = false
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            synchronized(focusLock) {
                resumeOnFocusGain = true
                playbackDelayed = false
            }
            pausePlayback()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // ... 暫停或回避取決于你的應用程序
        }
    }
}

3.3 自動降低音量

在 Android 8.0(API 26)中,當其他應用使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 請求焦點時,系統可以在不調用應用的 onAudioFocusChange() 回調的情況下降低和恢復音量。
雖然自動降低音量的行為對于音樂和視頻播放應用來說是可接受的,但在播放語音內容時(例如在聽書應用)就沒什么用處了。在這種情況下,應用應該暫停播放。
如果希望應用在被要求降低音量時暫停播放,應該創建包含 onAudioFocusChange() 回調方法的 OnAudioFocusChangeListener,該回調方法可以實現所需的暫停/恢復行為。 調用 setOnAudioFocusChangeListener() 來注冊監聽器,然后調用 setWillPauseWhenDucked(true) 告訴系統使用的回調,而不是執行自動降低音量。

3.4 延遲獲取焦點

在有些情況下,系統不能批準對音頻焦點的請求,因為焦點被其他應用“鎖定”了,例如在通話過程中。在這種情況下,requestAudioFocus() 會返回 AUDIOFOCUS_REQUEST_FAILED。在這種情況下,應用將不會播放音頻,因為它未獲得焦點。
方法setAcceptsDelayedFocusGain(true)可讓應用異步處理焦點請求。設置此標記后,在焦點鎖定時發出的請求會返回AUDIOFOCUS_REQUEST_DELAYED。當鎖定音頻焦點的情況不再存在時(例如當通話結束時),系統會批準待處理的焦點請求,并調用onAudioFocusChange()來通知應用。
為了處理“延遲獲取焦點”,必須創建包含onAudioFocusChange()回調方法的OnAudioFocusChangeListener,該回調方法會通過調用 setOnAudioFocusChangeListener()來實現所需行為并注冊監聽器。

3.5 響應音頻焦點更改

當應用獲得音頻焦點后,它必須能夠在其他應用為自己請求音頻焦點時釋放該焦點。出現這種情況時,應用會收到對AudioFocusChangeListener中的onAudioFocusChange()方法的調用,該方法是應用調用requestAudioFocus()時指定的。傳遞給onAudioFocusChange()focusChange參數表示所發生的更改類型。它對應于獲取焦點的應用所使用的持續時間提示。應用應該做出適當的響應。

3.5.1 暫時性失去焦點

如果焦點更改是暫時性的AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCKAUDIOFOCUS_LOSS_TRANSIENT,應用應該降低音量(如果不依賴于自動降低音量)或暫停播放,否則保持相同的狀態。在暫時性失去音頻焦點時,應該繼續監控音頻焦點的變化,并準備好在重新獲得焦點后恢復正常播放。當搶占焦點的應用放棄焦點時,收到一個回調AUDIOFOCUS_GAIN。此時,可以將音量恢復到正常水平或重新開始播放。

3.5.2 永久性失去焦點

如果是永久性失去音頻焦點 (AUDIOFOCUS_LOSS),則其他應用會播放音頻。您的應用應立即暫停播放,因為它不會收到AUDIOFOCUS_GAIN回調。要重新開始播放,用戶必須執行明確的操作,例如在通知或應用界面中按播放傳輸控件。
以下代碼段展示了如何實現OnAudioFocusChangeListener及其onAudioFocusChange()回調。請注意這里使用Handler延遲了對永久性失去音頻焦點的停止回調。

private val handler = Handler()
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
          AudioManager.AUDIOFOCUS_LOSS -> {
                // 永久性失去音頻焦點,立即暫停播放
                mediaController.transportControls.pause()
                // 等待30秒后停止播放
                handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30))
          }
          AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
                // 暫停播放
          }
          AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                // 降低音量,繼續播放
          }
          AudioManager.AUDIOFOCUS_GAIN -> {
                // 再次被授予音頻焦點,將音量調至正常,必要時重新開始播放
          }
   }
}

private var delayedStopRunnable = Runnable {
        mediaController.transportControls.stop()
}

為了確保在用戶重新開始播放時不會觸發延遲停止,請調用 mHandler.removeCallbacks(mDelayedStopRunnable)來響應任何狀態變化。例如,在回調的onPlay()onSkipToNext() 等中調用 removeCallbacks()。此外,在清理服務使用的資源時,也應該在服務的 onDestroy() 回調中調用此方法。

3.6 AudioFocus 常用常量

常量 備注
AUDIOFOCUS_NONE 沒有獲得、丟失音頻焦點,也沒有請求音頻焦點。
AUDIOFOCUS_GAIN 用于請求持續時間未知的音頻焦點。此參數會觸發其他監聽器的AudioManager.AUDIOFOCUS_LOSS。
AUDIOFOCUS_GAIN_TRANSIEN 用于請求短暫性音頻焦點。此參數會觸發其他監聽器的AudioManager.AUDIOFOCUS_LOSS_TRANSIENT。
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 用于申請短暫性音頻焦點,一般是錄音或者語音識別,此參數會觸發其他監聽器的AudioManager.AUDIOFOCUS_LOSS_TRANSIENT。
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 用于申請短暫性音頻焦點并要求其它應用降低音量,此時會混音播放,此參數會觸發其他監聽器的AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK。
AUDIOFOCUS_REQUEST_DELAYED 用于申請延遲授予的音頻焦點:請求成功,但只有在阻止立即授予的條件結束后,才會授予請求者音頻焦點。
AUDIOFOCUS_LOSS 音頻焦點丟失。
AUDIOFOCUS_LOSS_TRANSIENT 暫時失去音頻焦點。
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 暫時失去音頻焦點,失去音頻焦點的程序如果想繼續播放,可以降低輸出音量,因為新的焦點所有者不要求其他人保持沉默。
AUDIOFOCUS_REQUEST_FAILED 音頻焦點請求失敗。
AUDIOFOCUS_REQUEST_GRANTED 音頻焦點請求成功。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379