一步步自定義視訊播放器——使用SurfaceView播放視訊
Surface
Surface與SurfaceView這篇文章對Surface和SurfaceView做了很詳細的解釋。
簡單的說Surface對應了一塊螢幕緩衝區,每個window對應一個Surface,任何View都要畫在Surface的Canvas上。傳統的view共享一塊螢幕緩衝區,所有的繪製必須在UI執行緒中進行。
Surface可以理解為: Surface類似一個控制代碼,可以得到Canvas、原始緩衝區以及其他方面的內容。
Canvas用於畫圖
原始緩衝區用於儲存當前視窗的畫素資料
SurfaceView
SurfaceView繼承自View,但它有自己的Surface。
if (mWindow == null) {
mWindow = new MyWindow(this);
mLayout.type = mWindowType;
mLayout.gravity = Gravity.LEFT|Gravity.TOP;
mSession.addWithoutInputChannel(mWindow, mWindow.mSeq, mLayout,
mVisible ? VISIBLE : GONE, mContentInsets);
}
源碼錶明,SurfaceView會自己建立一個MyWindow,一個window對應一個Surface,因此SurfaceView也就內嵌了一個自己的Surface。
傳統View及其派生類的更新只能在UI執行緒,然而UI執行緒還同時處理其他互動邏輯,這就無法保證View更新的速度和幀率了,而SurfaceView可以用獨立的執行緒進行繪製,因此可以提供更高的幀率,例如遊戲,攝像頭取景等場景就比較適合SurfaceView來實現。
SurfaceHolder
SurfaceHolder是一個介面,其作用就像一個關於Surface的監聽器,提供訪問和控制SurfaceView內嵌的Surface相關的方法。
它通過三個回撥方法,讓我們可以感知到Surface的建立、銷燬或者改變。
在SurfaceView中有一個方法getHolder,可以很方便地獲得SurfaceView內嵌的Surface所對應的監聽器介面SurfaceHolder。
SurfaceHolder.Callback主要是當底層的Surface被建立、銷燬或者改變時提供回撥通知,由於繪製必須在Surface被建立後才能進行,因此SurfaceHolder.Callback中的surfaceCreated 和surfaceDestroyed 就成了繪圖處理程式碼的邊界。
SurfaceHolder.Callback中定義了三個介面方法:
- abstract void surfaceChanged(SurfaceHolder holder, int format, int width, int height):當surface發生任何結構性的變化時(格式或者大小),該方法就會被立即呼叫。
- abstract void surfaceCreated(SurfaceHolder holder):當surface物件建立後,該方法就會被立即呼叫。
- abstract void surfaceDestroyed(SurfaceHolder holder):當surface物件在將要銷燬前,該方法會被立即呼叫。
小結
- SurfaceView是一個擁有獨立繪圖層的特殊View
- Surface是記憶體中的一段繪圖緩衝區
- SurfaceView中具有兩個Surface, 即雙緩衝機制
- SurfaceHolder是Surface的持有者,SurfaceView就是通過過SurfaceHolder來對Surface進行管理控制的。並且SurfaceView.getHolder方法可以獲取SurfaceView相應的SurfaceHolder。
- Surface是在SurfaceView所在的Window可見的時候建立的。我們可以使用SurfaceHolder.addCallback方法來監聽Surface的建立與銷燬的事件。
從設計模式的高度來看,Surface、SurfaceView和SurfaceHolder實質上就是廣為人知的MVC,即Model-View-Controller。
Model就是模型的意思,或者說是資料模型,或者更簡單地說就是資料,也就是這裡的Surface;
View即檢視,代表使用者互動介面,也就是這裡的SurfaceView;
SurfaceHolder很明顯可以理解為MVC中的Controller(控制器)。
SurfaceView與MediaPlayer結合
SurfaceView解析視訊的流程:首先確定視訊的格式,知道編碼格式後,通過編碼格式進行解碼,得到一幀一幀的影象,並把這些影象快速的顯示在介面上。
簡單地說,SurfaceView用來顯示MediaPlayer中解析得到的視訊影象。
MediaPlayer通過setDisplay(SurfaceHolder sh)來指定SurfaceView顯示影象。
SurfaceView雙緩衝
SurfaceView和大部分視訊應用一樣,把視訊解析成的一幀幀影象進行顯示。如果把這個解析放在一個執行緒中完成,可能在上一幀顯示後,下一幀來不及解析,導致畫面不流暢或者視訊不同步。通過雙緩衝機制來顯示影象能夠有效解決上述問題。
雙緩衝機制可以理解為兩個執行緒輪番解析視訊流的幀影象,當一個執行緒解析完幀影象後,把影象渲染到介面中,同時另外一個執行緒開始解析下一幀影象,使得兩個執行緒輪番配合區解析視訊流,達到流暢播放的效果。
注意
SurfaceView內部實現了雙緩衝的機制,但是實現這個功能是非常消耗系統記憶體的。因為移動裝置的侷限性,Android在設計的時候規定,SurfaceView如果為使用者可見的時候,建立SurfaceView的SurfaceHolder,如果發現SurfaceView變為使用者不可見的時候,則立即銷燬SurfaceView的SurfaceHolder,以達到節約系統資源的目的。
如果開發人員不對SurfaceHolder進行維護,會出現最小化程式後,再開啟應用的時候,視訊的聲音在繼續播放,但是不顯示畫面了的情況,這就是因為當SurfaceView不被使用者可見的時候,之前的SurfaceHolder已經被銷燬了,再次進入的時候,介面上的SurfaceHolder已經是新的SurfaceHolder了。所以SurfaceHolder需要我們開發人員去編碼維護,維護SurfaceHolder需要用到上述的三個回撥方法。
小例子
使用的時候注意要配置許可權
<uses-permission android:name="android.permission.INTERNET" />
使用步驟為:
1.建立 MediaPlayer,並讓他載入指定的視訊檔案
2.在佈局中定義SurfaceView元件,或者在程式中建立SurfaceView元件
3.為SurfaceView的SurfaceHolder新增Callback監聽器
4.呼叫MediaPlayer物件的setDisplay(SurfaceHolder sh)將所播放的視訊影象輸出到指定的SurfaceView元件中
5.呼叫MediaPlayer物件的start(),stop(),和pause()方法。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化
mediaPlayer = new MediaPlayer();
surfaceView = findViewById(R.id.surface_view);
//設定螢幕常亮
surfaceView.getHolder().setKeepScreenOn(true);
//添加回調介面
surfaceView.getHolder().addCallback(callback);
}
public void play() {
try {
mediaPlayer.reset();//重置
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(URL);//設定播放路徑
mediaPlayer.setDisplay(surfaceView.getHolder());//視訊輸出到SurfaceView上
mediaPlayer.prepare();//使用同步方式
mediaPlayer.start();//開始播放
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "路徑錯誤", Toast.LENGTH_SHORT).show();
}
}
完整程式碼
package com.example.com.myapplication;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Toast;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private SurfaceView surfaceView;
private MediaPlayer mediaPlayer;
private String URL = "http://fairee.vicp.net:83/2016rm/0116/baishi160116.mp4";
private int position;//記錄位置
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化
mediaPlayer = new MediaPlayer();
surfaceView = findViewById(R.id.surface_view);
//設定螢幕常亮
surfaceView.getHolder().setKeepScreenOn(true);
//添加回調介面
surfaceView.getHolder().addCallback(callback);
}
private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
position = mediaPlayer.getCurrentPosition();
mediaPlayer.stop();
}
}
};
public void play() {
try {
mediaPlayer.reset();//重置
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
//raw資料夾下面的內容
// Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.baishi);
// mediaPlayer.setDataSource(this, uri);
mediaPlayer.setDisplay(surfaceView.getHolder());
mediaPlayer.setDataSource(URL);
//視訊輸出到SurfaceView上
mediaPlayer.prepare();//使用同步方式
mediaPlayer.start();//開始播放
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(MainActivity.this, "路徑錯誤", Toast.LENGTH_SHORT).show();
}
}
public void start(View view) {
play();
}
public void pause(View view) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
position = mediaPlayer.getCurrentPosition();
}
}
public void goOn(View view) {
mediaPlayer.seekTo(position);
mediaPlayer.start();
}
public void stop(View view) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
}
@Override
protected void onPause() {
super.onPause();
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
position = mediaPlayer.getCurrentPosition();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//釋放資源
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
mediaPlayer.release();
}
}
}
佈局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.com.myapplication.MainActivity">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="400dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="start"
android:text="播放" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="pause"
android:text="暫停" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="goOn"
android:text="繼續" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="stop"
android:text="停止" />
</LinearLayout>
</LinearLayout>
實現效果: