隨著快手空盼,抖音,西瓜視頻等視頻APP的崛起象迎,視頻播放已經(jīng)成為主流,此時(shí)做為Android研發(fā)的你呛踊,想要提升本身的能力還不知道怎么開(kāi)發(fā)視頻播放器怎么行砾淌?
視頻播放器有原生的VideoView、開(kāi)源的Ijkplayer?谭网、ExoPlayer汪厨、JieCaoVideoPlayer等等,這些部分的播放器我們?cè)陂_(kāi)發(fā)過(guò)程中蜻底,使用過(guò)這些視頻播放框架來(lái)播放本地視頻或者網(wǎng)絡(luò)視頻骄崩,但不一定會(huì)滿足業(yè)務(wù)需求的。因此薄辅,我們可以去自定義一個(gè)播放器要拂,在開(kāi)發(fā)Android應(yīng)用的過(guò)程中,難免需要自定義View站楚,其實(shí)自定義View不難脱惰,只要了解原理,實(shí)現(xiàn)起來(lái)就沒(méi)有那么難窿春,從而滿足業(yè)務(wù)需求拉一。
先看效果圖采盒。
我自定義一個(gè)VideoPlayerView類(lèi),主要是由MediaPlayer與SurfaceView相結(jié)合組成一個(gè)視頻播放器蔚润,從Android原生的VideoView視頻框架磅氨,也是基于實(shí)現(xiàn)的。
1.首先編寫(xiě)布局文件layout_video_player.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:orientation="vertical"
>
<SurfaceView
android:id="@+id/surfaceview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:layout_alignParentBottom="true"
>
<TextView
android:id="@+id/tvCurrentTime"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.1"
android:gravity="center|right"
android:text="00:00"
android:textSize="12dp"
android:textColor="#FFFFFFFF"
/>
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="10dp"
android:layout_weight="0.9"
android:progressDrawable="@drawable/seekbar_progress_drawable"
android:maxHeight="2dp"
android:minHeight="1dp"
android:thumb="@drawable/round_bg"
android:layout_gravity="center"
/>
<TextView
android:id="@+id/tvTotalTime"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.1"
android:gravity="center|left"
android:text="00:00"
android:textSize="12dp"
android:textColor="#FFFFFFFF"
/>
</LinearLayout>
<Button
android:id="@+id/player"
android:layout_width="45dp"
android:layout_height="45dp"
android:background="@drawable/ic_play_24"
android:layout_centerInParent="true"
/>
</RelativeLayout>
</FrameLayout>
2.編寫(xiě)VideoPlayerView.java文件
public class VideoPlayerView extends FrameLayout implements View.OnClickListener {
private static final String TAG = "VideoPlayerView";
private SurfaceView mSurfaceView;
private MediaPlayer mMediaPlayer = null;
private int currentPosition = 0;
private SurfaceHolder mSurfaceHolder;
private boolean isPlaying;
private SeekBar mSeekBar;
private Button mPlyer;
private String mUrl;
private TextView tvCurrentTime, tvTotalTime;
private Handler handler = new Handler(Looper.getMainLooper());
public VideoPlayerView(Context context) {
this(context, null);
}
public VideoPlayerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public VideoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
initView(context);
// 為SurfaceHolder添加回調(diào)
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(callback);
//設(shè)置Surface不維護(hù)自己的緩沖區(qū)嫡纠,而是等待屏幕的渲染引擎將內(nèi)容推送到界面
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
private void initView(Context context) {
View view = LayoutInflater.from(context).inflate(R.layout.layout_video_player,null,false);
mSurfaceView = view.findViewById(R.id.surfaceview);
mSeekBar = view.findViewById(R.id.seekBar);
mPlyer = view.findViewById(R.id.player);
tvCurrentTime = view.findViewById(R.id.tvCurrentTime);
tvTotalTime = view.findViewById(R.id.tvTotalTime);
addView(view);
mPlyer.setOnClickListener(this);
mSeekBar.setOnSeekBarChangeListener(change);
}
private SurfaceHolder.Callback callback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
LogUtil.info(TAG, "surfaceCreated被創(chuàng)建");
if (currentPosition > 0) {
// 創(chuàng)建SurfaceHolder的時(shí)候烦租,如果存在上次播放的位置,則按照上次播放位置進(jìn)行播放
play(currentPosition);
currentPosition = 0;
}
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
LogUtil.info(TAG, "surfaceChanged大小改變");
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
LogUtil.info(TAG, "surfaceDestroyed被銷(xiāo)毀");
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
currentPosition = mMediaPlayer.getCurrentPosition();
mMediaPlayer.stop();
mMediaPlayer.release();
}
}
};
private void play(int currentPosition) {
if (mMediaPlayer != null) {
mMediaPlayer.seekTo(currentPosition);
}
}
/**
* 播放本地視頻
* @param mUrl
*/
public void setPlayerVideo(String mUrl) {
this.mUrl = mUrl;
LogUtil.info(TAG, "setPlayerVideo mUrl: " + mUrl);
if (mUrl.contains("http")) {
showRemoteVideo(0);
} else {
playerLocalView(0);
}
}
// 播放本地視頻
private void playerLocalView(final int msec) {
LogUtil.info(TAG, " 獲取視頻文件地址");
String path = mUrl;
File file = new File(path);
if (!file.exists()) {
Toast.makeText(AppContextUtil.getContext(), "視頻文件路徑錯(cuò)誤", Toast.LENGTH_SHORT).show();
return;
}
LogUtil.info(TAG, "指定視頻源路徑");
try {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 設(shè)置播放的視頻源
mMediaPlayer.setDataSource(file.getAbsolutePath());
// 設(shè)置顯示視頻的SurfaceHolder
mMediaPlayer.setDisplay(mSurfaceHolder);
LogUtil.info(TAG, "開(kāi)始裝載");
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
LogUtil.info(TAG, "裝載完成");
mMediaPlayer.start();
// 按照初始位置播放
mMediaPlayer.seekTo(msec);
// 設(shè)置進(jìn)度條的最大進(jìn)度為視頻流的最大播放時(shí)長(zhǎng)
mSeekBar.setMax(mMediaPlayer.getDuration());
tvCurrentTime.setText(DateUtil.getDate(msec, DateUtil.PARAMETER));
tvTotalTime.setText(DateUtil.getDate(mMediaPlayer.getDuration(), DateUtil.PARAMETER));
// 開(kāi)始線程除盏,更新進(jìn)度條的刻度
new Thread() {
@Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
// 如果正在播放叉橱,沒(méi)0.5.毫秒更新一次進(jìn)度條
int current = mMediaPlayer.getCurrentPosition();
mSeekBar.setProgress(current);
handler.post(new Runnable() {
@Override
public void run() {
tvCurrentTime.setText(DateUtil.getDate(current, DateUtil.PARAMETER));
}
});
sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// 在播放完畢被回調(diào)
mPlyer.setEnabled(true);
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 發(fā)生錯(cuò)誤重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// 播放遠(yuǎn)程視頻
private void showRemoteVideo(final int msec) {
String videoUrl2 = mUrl;
Uri uri = Uri.parse(videoUrl2);
try {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 設(shè)置播放的視頻源
mMediaPlayer.setDataSource(AppContextUtil.getContext(), uri);
// 設(shè)置顯示視頻的SurfaceHolder
mMediaPlayer.setDisplay(mSurfaceHolder);
LogUtil.info(TAG, "開(kāi)始裝載");
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
LogUtil.info(TAG, "裝載完成");
mMediaPlayer.start();
// 按照初始位置播放
mMediaPlayer.seekTo(msec);
// 設(shè)置進(jìn)度條的最大進(jìn)度為視頻流的最大播放時(shí)長(zhǎng)
mSeekBar.setMax(mMediaPlayer.getDuration());
tvCurrentTime.setText(DateUtil.getDate(msec, DateUtil.PARAMETER));
tvTotalTime.setText(DateUtil.getDate(mMediaPlayer.getDuration(), DateUtil.PARAMETER));
// 開(kāi)始線程,更新進(jìn)度條的刻度
new Thread() {
@Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
// 如果正在播放者蠕,沒(méi)0.5.毫秒更新一次進(jìn)度條
int current = mMediaPlayer.getCurrentPosition();
mSeekBar.setProgress(current);
handler.post(new Runnable() {
@Override
public void run() {
tvCurrentTime.setText(DateUtil.getDate(current, DateUtil.PARAMETER));
}
});
sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
}
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mp.release();
mp = null;
// 在播放完畢被回調(diào)
mPlyer.setEnabled(true);
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 發(fā)生錯(cuò)誤重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private SeekBar.OnSeekBarChangeListener change = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// 當(dāng)進(jìn)度條停止修改的時(shí)候觸發(fā)
// 取得當(dāng)前進(jìn)度條的刻度
int progress = seekBar.getProgress();
// if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
// 設(shè)置當(dāng)前播放的位置
mMediaPlayer.seekTo(progress);
tvCurrentTime.setText(DateUtil.getDate(progress, DateUtil.PARAMETER));
// }
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
}
};
@Override
public void onClick(View v) {
if (v.getId() == R.id.player) {
if (mMediaPlayer == null) {
return;
}
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mPlyer.setBackgroundResource(R.drawable.ic_play_24);
} else {
mMediaPlayer.start();
mPlyer.setBackgroundResource(R.drawable.ic_pause_24);
}
}
}
public void pause () {
if (mMediaPlayer == null) {
return;
}
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mPlyer.setBackgroundResource(R.drawable.ic_pause_24);
}
}
public void destroy() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
isPlaying = false;
LogUtil.info(TAG, "MediaPlayer is release");
}
}
該類(lèi)主要做了如下工作:
1).初始化控件窃祝;
2).SurfaceHolder回調(diào)監(jiān)聽(tīng);
3).播放本地視頻踱侣;
4).播放網(wǎng)絡(luò)視頻粪小;
5).常用SeekBar顯示進(jìn)度及改變進(jìn)度;
6).額外工作泻仙,播放糕再、暫停、總時(shí)長(zhǎng)玉转、當(dāng)前時(shí)長(zhǎng)的View控件突想。
3.編寫(xiě)Activity的布局文件activity_player_video.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/player_layout"
/>
從上面可以看出,里面沒(méi)有任何子view究抓,僅僅聲明FrameLayout和id即可猾担。
4.PlayerVideoActivity.java
public class PlayerVideoActivity extends Activity {
private FrameLayout playerLayout;
private VideoPlayerView videoPlayerView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player_video);
initPlayer();
}
private void initPlayer() {
String localPath = Environment.getExternalStorageDirectory() +"/xlk-player.mp4";
playerLayout = findViewById(R.id.player_layout);
videoPlayerView = new VideoPlayerView(this);
playerLayout.addView(videoPlayerView);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//這里傳入本地視頻的路徑或者網(wǎng)絡(luò)視頻的url
videoPlayerView.setPlayerVideo(localPath);
}
},500);
}
@Override
protected void onPause() {
super.onPause();
if (videoPlayerView != null) {
videoPlayerView.pause();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (videoPlayerView != null) {
videoPlayerView.destroy();
videoPlayerView = null;
}
}
}
上面實(shí)例化一個(gè)VideoPlayerView,然后通過(guò)VideoPlayerView對(duì)象來(lái)調(diào)用setPlayerVideo的方法傳入本地視頻的路徑或者網(wǎng)絡(luò)視頻的url,實(shí)現(xiàn)音視頻播放器刺下,運(yùn)行此項(xiàng)目绑嘹,效果如開(kāi)頭顯示的一幕。
自定義View播放器已經(jīng)實(shí)現(xiàn)了橘茉,但我們需要掌握MediaPlayer的初始化過(guò)程是怎么樣的工腋,MediaPlayer做了哪些工作,音視頻數(shù)據(jù)如何渲染到SurfaceView上面的畅卓,這需要把MediaPlayer框架原理搞清楚擅腰。就像下面的。
每個(gè)方法調(diào)用過(guò)程是怎么樣子的翁潘,做了哪些工作趁冈,而不是簡(jiǎn)單寫(xiě)幾行代碼就可以實(shí)現(xiàn)播放視頻或者音頻。
小伙伴們有興趣的話,搜索并關(guān)注公眾號(hào)“Android技術(shù)迷”關(guān)注后可閱讀更多文章渗勘,感謝各位關(guān)注沐绒。