話說音視頻播放現在真是不搞不行啦,不光我們要從市面上 N 多的播放器中找出好用的,熟悉其 API ,甚至還需要我們能利用開源解碼器來編寫我們自己的視頻播放器,模塊,這點在越來越不一樣,差異化的播放器中可以看得見,很多 app 不光播放器 UI 不一樣,甚至很多邏輯都不同
所以大家努力吧,先不求能搞定 FFMG 編解碼等底層技術,至少我們現在得能利用 ijkPlayer 編寫自己的視頻播放器出來,才能徹底搞得定 app 端的播放需求,要是打算自己上直播推流 sdk ,那么學的就更多了,都是得按年來計算了,音視頻水太神了,至少要求我們得搞定 UI 部分才行,在視頻這塊用別人開源的播放器是走不遠的,因為需求差異性太大
錄制音視頻 AudioRecord/MediaRecord
視頻的基礎知識點推薦看下面:
- Android視頻開發進階(part1-關于視頻的那些術語)
- Android視頻開發進階(part2-MP4文件的解析)
- Android視頻開發進階(part3-Android的Media API)
- Android視頻開發進階(part4-自適應視頻播放技術(Adaptive Streaming))
- Android視頻開發進階(part5-ExoPlayer分析1,ExoPlayer的handler)
- Android視頻開發進階(part6-安卓的DRM,視頻版權保護)
視頻流媒體協議有哪些
- HTTP
-> 最傳統的視頻流媒體,不支持實時流媒體的播放 - RTMP
-> Adobe 公司用于 flash 播放的 - RTSP
-> android 原生支持此種流媒體協議 - FLV
- HLS
-> apple 公司開發,把視頻分成 3-10 秒的小段,下發 m3u8 文件來標記文件順序,使用 HTTP、HTTPS 傳輸 - MMS
-> 微軟公司開發的
官方原生播放器 MediaPlayer
MediaPlayer 是 Androd 多媒體框架中的一個重要組件,通過該類,我們可以解碼和播放音視頻,但是 MediaPlayer 本身只支持 音頻 播放,需要傳入專門的視頻承載 view 才能播放視頻
MediaPlayer 可以支持三種不同的媒體來源:
- 本地資源
- 內部URI,比如你可以通過ContentResolver來獲取
- 外部URL(流)
MediaPlayer支持兩種流媒體協議,HTTP 和 RTSP,這兩種協議最大的不同是,RTSP 協議支持實時流媒體的播放,而 HTTP 協議不支持
原生 MediaPalyer 支持的協議和封裝格式實在太有限了,如果我們想播放那些它不支持的視頻,這時候就需要第三方播放器了,很多第三方播放器的底層實現都是基于 FFmpeg
MediaPlayer 對于視頻格式支持的也是非常少,值能支持: MP4,AVI,3DP 這3個早期的手機品是格式
開源的音視頻解碼器在 API 上都參考了 MediaPlayer ,所以學習 MediaPlayer API 就是我們的第一步
- 創建 MediaPlayer 對象,可以直接和本地資源綁定
MediaPlayer mp = new MediaPlayer();
MediaPlayer mp = MediaPlayer.create(this, R.raw.test); //無需再調用setDataSource
create(Context context, Uri uri, SurfaceHolder holder)
- 設置播放資源
MediaPlayer.create(this, R.raw.test); // raw下的資源
mp.setDataSource("/sdcard/test.mp3"); // 本地文件路徑
mp.setDataSource("http://www.xxx.com/music/test.mp3");// 網絡URL文件
- 播放資源
// uri 資源
Uri myUri = ....; /**initialize Uri here*/
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(getApplicationContext(), myUri);
mediaPlayer.prepare();
mediaPlayer.start();
// 網絡文件
String url = "http://........"; // your URL here
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.prepareAsync()
mediaPlayer.setOnPreparedListener({
// 加載完畢再開始播放
mediaPlayer.start()
})
- MediaPlayer 主要 API,需要熟知
getCurrentPosition( ):得到當前的播放位置
getDuration() :得到文件的時間
getVideoHeight() :得到視頻高度
getVideoWidth() :得到視頻寬度
isLooping():是否循環播放
isPlaying():是否正在播放
pause():暫停
prepare():準備(同步)
prepareAsync():準備(異步)
release():釋放MediaPlayer對象
reset():重置MediaPlayer對象
seekTo(int msec):指定播放的位置(以毫秒為單位的時間)
setAudioStreamType(int streamtype):指定流媒體的類型
setDisplay(SurfaceHolder sh):設置用SurfaceHolder來顯示多媒體
setLooping(boolean looping):設置是否循環播放
setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener):網絡流媒體的緩沖監聽
setOnCompletionListener(MediaPlayer.OnCompletionListener listener):網絡流媒體播放結束監聽
setOnErrorListener(MediaPlayer.OnErrorListener listener):設置錯誤信息監聽
setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener listener):視頻尺寸監聽
setScreenOnWhilePlaying(boolean screenOn):設置是否使用SurfaceHolder顯示
setVolume(float leftVolume, float rightVolume):設置音量
start():開始播放
stop():停止播放
我們需要熟知下面這張 MediaPalyer 解碼器生命周期圖,所有的開源項目都是按這個思路來做的
狀態1:Idel(空閑)狀態
當 mediaplayer創建或者執行reset()方法后處于這個狀態。狀態2:Initialized(已初始化)狀態
當調用mediaplayer的setDataResource()方法給mediaplayer設置播放的數據源后,mediaplayer會處于該狀態。狀態3:Prepared(準備就續)狀態
設置完數據源后,調用mediaplayer的prepare()方法,讓mediaplayer準備播放。值得一提的是,這里除了prepare()方法,還有prepareAsnyc()方法,此方法是異步方法,一般用于網絡視頻的緩沖。當緩沖完畢后,就會觸發準備完畢的事件。我們要做的就是監聽該事件(OnPreparedListener),當緩沖完成時,執行相應的操作。在此狀態上,我們可以調用seekTo()方法定位視頻,此方法不改變mediaplayer的狀態;亦可調用stop()放棄視頻播放,使mediaplayer處于Stopped狀態。一般我們會在此狀態上調用start()方法開始播放視頻。狀態4:Started(開始)狀態
當處于Prepared狀態、Paused狀態和PlayebackCompeleted狀態時,調用Started()方法即可進入該狀態。在該狀態中,mediaplayer開始播放視頻,可以通過seekTo()方法和start()方法改變視頻播放的進度,當Looping為真且播放完畢后,它會重新開始播放(即循環播放);否則播放完畢后,會觸發事件并調用OnCompletionaListener.OnCompletion()方法,進行特定操作,并進入PlaybackCompleted狀態。在此狀態中,亦可調用pause()方法或者stop()方法讓視頻暫?;蛲V梗藭rmediaplayer分別處于Stopped和Paused狀態。狀態5:Stopped(停止)狀態
當 mediaplayer處于Prepared、Started、Paused、PlaybackCompleted狀態時,調用stop()方法即可進入本狀態。應特別注意的是,在本狀態中,若想重新開始播放,不能直接調用start()方法,必須調用prepare()方法或prepareAsync()方法重新讓mediaplayer處于Prepared狀態方可調用start()方法播放視頻。狀態6:Paused(暫停)狀態
當mediaplayer處于Started狀態是,調用pause()方法即可進入本狀態。在本狀態里,可直接調用start()方法使,mediaplayer回到Started狀態,亦可調用stop()方法停止視頻播放,讓播放器處于停止態。狀態7:PlaybackCompleted(播放完成)狀態
當mediaplayer播放完成且Looping為假時即可進入本狀態。在本狀態可調用start()方法使mediaplayer回到Started狀態(注意此時是從頭開始播放);亦可調用stop()方法使mediaplayer處于停止態,結束播放。狀態8:Error(錯誤)狀態
當mediaplayer出現錯誤時處于此狀態。
SurfaceView , TextureView
SurfaceView , TextureView 都是 android 中用于承載視頻幀顯示的 view ,熟悉這2個 view 的大家都知道,這 2個 view 都是異步的,都是再非 UI 線程中計算的,擁有獨立 surface 顯存的,這是因為視頻播放任務太重,UI 線程 hold 不住
SurfaceView
大概原理就是在現有View的位置上創建一個新的 Window,內容的顯示和渲染都在新的 Window 中。這使得 SurfaceView 的繪制和刷新可以在單獨的線程中進行,從而大大提高效率。但是呢,由于 SurfaceView 的內容沒有顯示在View中而是顯示在新建的 Window中, 使得 SurfaceView 的顯示不受 View 的屬性控制,不能進行平移,縮放等變換,也不能放在其它 RecyclerView 或 ScrollView中,一些 View 中的特性也無法使用。TextureView
TextureView 是在4.0(API level 14)引入的,與 SurfaceView 相比,它不會創建新的窗口來顯示內容。它是將內容流直接投放到View中,并且可以和其它普通View一樣進行移動,旋轉,縮放,動畫等變化。TextureView必須在硬件加速的窗口中使用。TextureView被創建后不能直接使用,必須要在它被它添加到ViewGroup后,待SurfaceTexture準備就緒才能起作用(看TextureView的源碼,TextureView是在繪制的時候創建的內部SurfaceTexture。SurfaceTexture的準備就緒、大小變化、銷毀、更新等狀態變化時都會回調相對應的方法。當TextureView內部創建好SurfaceTexture后,在監聽器的onSurfaceTextureAvailable方法中,用SurfaceTexture來關聯MediaPlayer,作為播放視頻的圖像數據來源。SurfaceTexture作為數據通道,把從數據源(MediaPlayer)中獲取到的圖像幀數據轉為GL外部紋理,交給TextureVeiw作為View heirachy中的一個硬件加速層來顯示,從而實現視頻播放功能。
視頻播放方式一:VideoView + MediaController
這是官方原生的實現,VideoView 繼承 SurfaceView ,內部封裝了 SurfaceView 的所有操作,MediaController 這樣來顯示視頻控制相關的 view 部分
我個人是非常喜歡官方的 API 設計的,代碼功能,層次分離,起名都非常規范,讓人一看就懂,用起來也是很爽,奈何就是支持的流媒體格式太少,我們只能去使用開源組件
VideoView + MediaController 的簡單使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.play);
videoview = (VideoView) findViewById(R.id.video);
mMediaController = new MediaController(this);
videoview.setMediaController(mMediaController);
mMediaController.setAnchorView(videoview)
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
loadView(url.getText().toString());
}
});
}
public void loadView(String path) {
Uri uri = Uri.parse(path);
videoview.setVideoURI(uri);
videoview.start;
videoview.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// mp.setLooping(true);
mp.start();// 播放
Toast.makeText(MainActivity.this, "開始播放!", Toast.LENGTH_LONG).show();
}
});
videoview.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Toast.makeText(MainActivity.this, "播放完畢", Toast.LENGTH_SHORT).show();
}
});
}
我們簡單看下 MediaController 是如何管理 視頻控制 view 的
MediaController 繼承 FrameLayout
public class MediaController extends FrameLayout
在 makeControllerView 中創建 控制布局 view ,并初始化 view
protected View makeControllerView() {
LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null);
initControllerView(mRoot);
return mRoot;
}
media_controller 樣式
在 setAnchorView 關聯視頻播放器 VideoView 時把 控制 view 添加到自己身上
public void setAnchorView(View view) {
if (mAnchor != null) {
mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener);
}
mAnchor = view;
if (mAnchor != null) {
mAnchor.addOnLayoutChangeListener(mLayoutChangeListener);
}
FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
removeAllViews();
View v = makeControllerView();
addView(v, frameParams);
}
視頻播放方式二:開源解碼器 + TextureView
我們注定是不會使用原生 VideoView 去遠程視頻的,沒辦法主流的流媒體格式 VideoView 都不支持,我們只能去使用 開源的視頻解碼器 + TextureView 去自己實現了
這就是我們后面需要自己的搞定的