在大型的app中,音视频的功能是必不可少的,尤其是视频,这是除了图片之外,更加直观的多媒体信息,因此对于视频播放组件,是每个app中不可缺少的,本小节将会从底层视频播放器取型,分析各类播放器的优缺点,并开发一个通用的播放器组件。
1、播放器选型
(1)VideoView
在之前做过的项目中,有使用过VideoView播放器组件,通常和MediaController配合使用,但是此类视频播放控件很难定制开发,而且界面并不美观,在大型app中显然不适合使用。
(2)MediaPlayer和SurfaceView
在api14之前,这一个组合,是可以取型的组合之一,定制度高,但是因为需要处理很多生命周期,使用起来比较复杂。
(3)MediaPlayer和TextureView
在api14之后,引入这个组合,该组合的功能要比之前的多,除了添加动画之外,还可以在ListView或者RecyclerView中使用,这类界面也是非常多见的,所以在定制视频播放组件的时候,此类组合是最佳选择。
2、视频播放器的生命周期
说是视频播放器的生命周期,倒不如具体说是MediaPlayer的生命周期,如下:
起始状态是IDEL,当调用setDataSource
方法的时候,开始加载视频信息,也就是完成初始化操作,这个时候有两个方法,prepare
方法和prepareAsync
方法,这两个方法分别为同步方法和异步方法,显然在这里要使用prepareAsync
,否则会一直阻塞主线程可能会导致应用崩溃,通过监听回调,通知主线程prepare完毕;
当主线程接收到onPrepared回调之后,就可以调用start方法开始播放视频,视频的一些状态例如暂停状态,可以和start状态相互切换;当视频播放完毕之后,就进入complete状态。
当视频加载失败的时候,可以调用stop方法,重新开始加载。
/**
* 自定义视频播放器,处理各类生命周期和事件触发
*/
public class CustomVideoView extends RelativeLayout implements MediaPlayer.OnPreparedListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener
, MediaPlayer.OnCompletionListener, TextureView.SurfaceTextureListener, View.OnClickListener {
/**
* constant
*/
private static final int STATE_ERROR = -1;
private static final int STATE_IDLE = 0;
private static final int STATE_PLAYING = 1;
private static final int STATE_PAUSE = 2;
private static final int LOAD_COUNT = 3;
private static final int MSG_WHAT = 0x01;
/**
* UI
*/
private RelativeLayout mPlayView;
private TextureView videoview;
private ImageView mFullBtn,mLoadingView;
private Button mPlayBtn;
private Surface videoSurface;
private MediaPlayer mediaPlayer;
/**
* data
*/
private AudioManager manager;
private String videoUrl;
//播放器的宽高
private int mVideoWidth,mVideoHeight;
//失败加载的次数
private int loadCount;
/**
* status
*/
private int mCurrentState = STATE_IDLE;
private boolean isRealPause;
private boolean isRealComplete;
private VideoPlayStatusChangedListener mListener;
private Handler mHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what){
case MSG_WHAT:
if(mListener != null){
mListener.BufferedUpdate(getCurrentPosition());
mHandler.sendEmptyMessageDelayed(MSG_WHAT,1000);
}
break;
}
}
};
public CustomVideoView(Context context) {
super(context);
manager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
initData();
initVideoView();
}
private void initVideoView() {
mPlayView = (RelativeLayout) LayoutInflater.from(getContext())
.inflate(R.layout.layout_videoview,this);
videoview = mPlayView.findViewById(R.id.videoview);
//设置监听
videoview.setSurfaceTextureListener(this);
videoview.setOnClickListener(this);
mFullBtn = mPlayView.findViewById(R.id.iv_full);
mFullBtn.setOnClickListener(this);
mPlayBtn = mPlayView.findViewById(R.id.btn_play);
mPlayBtn.setOnClickListener(this);
mLoadingView = mPlayView.findViewById(R.id.iv_loading);
LayoutParams params = new LayoutParams(mVideoWidth,mVideoHeight);
params.addRule(RelativeLayout.CENTER_IN_PARENT);
mPlayView.setLayoutParams(params);
Log.e("TAG","初始化View完成");
}
private void initData() {
DisplayMetrics dm = new DisplayMetrics();
WindowManager manager = (WindowManager)
getContext().getSystemService(Context.WINDOW_SERVICE);
manager.getDefaultDisplay().getMetrics(dm);
mVideoWidth = dm.widthPixels;
mVideoHeight = (int) (mVideoWidth * (9 / 16.0f));
}
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
}
/**
* 播放完成之后的回调
* @param mp
*/
@Override
public void onCompletion(MediaPlayer mp) {
//回到播放起点
playBack();
//设置状态
setRealPause(true);
setRealComplete(true);
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
//判断加载次数
if(loadCount >= LOAD_COUNT){
setCurrentState(STATE_ERROR);
//显示暂停按钮
showPlayOrPauseView(false);
//播放失败
if(mListener != null){
mListener.VideoLoadError();
}
}else{
//重新加载
stop();
}
return true;
}
/**
* 视频加载完成之后,回调此方法
* @param mp
*/
@Override
public void onPrepared(MediaPlayer mp) {
checkMediaPlayer();
//显示播放View
showPlayOrPauseView(true);
//开始加载
mediaPlayer.setOnBufferingUpdateListener(this);
//加载次数复位
loadCount = 0;
//设置为暂停状态
setCurrentState(STATE_PAUSE);
//开始播放
resume();
if(mListener != null){
mListener.VideoLoadSuccess();
}
}
/**
* 当TextureView准备好之后,才可以加载视频
* @param surface
* @param width
* @param height
*/
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
videoSurface = new Surface(surface);
checkMediaPlayer();
if(mediaPlayer != null){
mediaPlayer.setSurface(videoSurface);
}
Log.e("TAG","TextureView初始化完成");
//开始加载
load();
Log.e("TAG","开始加载");
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
public void setListener(VideoPlayStatusChangedListener mListener) {
this.mListener = mListener;
}
/**
* 视频加载
*/
public void load(){
//如果当前不是IDLE状态,不去处理
if(mCurrentState != STATE_IDLE){
return;
}
//显示加载View
showLoadingView(true);
try {
setCurrentState(STATE_IDLE);
//创建MediaPlayer
checkMediaPlayer();
//加载视频url地址
mediaPlayer.setDataSource(videoUrl);
//异步加载
mediaPlayer.prepareAsync();
Log.e("TAG","开始加载!");
}catch (Exception e){
stop();
Log.e("TAG","加载失败");
}
}
/**
* 视频播放
*/
public void resume(){
if(mCurrentState != STATE_PAUSE){
return;
}
//设置当前状态为播放状态
setCurrentState(STATE_PLAYING);
//显示播放按钮
showPlayOrPauseView(true);
if(mediaPlayer != null){
setRealPause(false);
//开始播放
mediaPlayer.start();
//发送进度消息
mHandler.sendEmptyMessage(MSG_WHAT);
}
}
/**
* 视频暂停
*/
public void pause(){
if(mCurrentState != STATE_PLAYING){
return;
}
//设置当前状态为PAUSE
setCurrentState(STATE_PAUSE);
if(mediaPlayer != null){
//显示暂停的View
showPlayOrPauseView(false);
//真正的暂停状态
setRealPause(true);
mediaPlayer.pause();
}
}
/**
* 视频停止
*/
public void stop(){
if(mediaPlayer != null){
mediaPlayer.reset();
mediaPlayer.setOnSeekCompleteListener(null);
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
//设置为初始状态
setCurrentState(STATE_IDLE);
//判断加载失败的次数
if(loadCount < LOAD_COUNT){
loadCount += 1;
//重新加载
load();
}else{
//加载失败
setCurrentState(STATE_PAUSE);
}
}
/**
* 视频销毁
*/
public void destroy(){
if(mediaPlayer != null){
mediaPlayer.setOnSeekCompleteListener(null);
mediaPlayer.reset();
mediaPlayer.release();
mediaPlayer = null;
}
}
/**
* 到达某处暂停
*/
public void seekAndPause(int position){
if(mediaPlayer != null){
//显示暂停按钮
showPlayOrPauseView(false);
//设置当前状态
setCurrentState(STATE_PAUSE);
setRealPause(true);
//设置监听
mediaPlayer.setOnSeekCompleteListener(null);
mediaPlayer.seekTo(position);
mediaPlayer.pause();
}
}
/**
* 创建MediaPlayer
*/
public void checkMediaPlayer(){
if(mediaPlayer == null){
mediaPlayer = createMediaPlayer();
}
}
private MediaPlayer createMediaPlayer() {
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setOnErrorListener(this);
mediaPlayer.setOnCompletionListener(this);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
return mediaPlayer;
}
/**
* 加载videoUrl
*/
public void setDataSource(String url){
this.videoUrl = url;
}
/**
* 显示播放或者暂停的View
*/
public void showPlayOrPauseView(boolean show){
mLoadingView.setVisibility(View.GONE);
mPlayBtn.setVisibility(show ? GONE : VISIBLE);
mFullBtn.setVisibility(show ? VISIBLE : GONE);
}
/**
* 显示全屏的View
*/
public void showFullBtn(boolean show){
mFullBtn.setVisibility(show ? VISIBLE : GONE);
}
/**
* 显示加载View
*/
public void showLoadingView(boolean show){
mLoadingView.setVisibility(show ? VISIBLE : GONE);
}
/**
* 得到当前的状态
* @return
*/
public int getCurrentState() {
return mCurrentState;
}
/**
* 设置当前视频播放的状态
* @param mCurrentState
*/
public void setCurrentState(int mCurrentState) {
this.mCurrentState = mCurrentState;
}
/**
* 是否真正的暂停
* @return
*/
public boolean isRealPause() {
return isRealPause;
}
public void setRealPause(boolean realPause) {
isRealPause = realPause;
}
/**
* 是否真正的结束播放
* @return
*/
public boolean isRealComplete() {
return isRealComplete;
}
public void setRealComplete(boolean realComplete) {
isRealComplete = realComplete;
}
/**
* 是否在播放视频
*/
public boolean isPlaying(){
if(mediaPlayer != null && mediaPlayer.isPlaying()
&& mCurrentState == STATE_PLAYING){
return true;
}
return false;
}
/**
* 得到当前的播放进度
*/
public int getCurrentPosition(){
if(mediaPlayer == null){
createMediaPlayer();
mediaPlayer.getCurrentPosition();
}
return mediaPlayer.getCurrentPosition();
}
/**
* 得到播放视频的总进度
*/
public int getDuration(){
if(mediaPlayer == null) {
createMediaPlayer();
mediaPlayer.getDuration();
}
return mediaPlayer.getDuration();
}
/**
* 回到播放起点
*/
public void playBack(){
if(mediaPlayer != null){
mediaPlayer.setOnSeekCompleteListener(null);
//回到播放起点
mediaPlayer.seekTo(0);
//显示暂停按钮
showPlayOrPauseView(false);
//设置当前播放状态
setCurrentState(STATE_PAUSE);
}
}
@Override
public void onClick(View v) {
if(v.getId() == R.id.btn_play){
if(mListener != null) mListener.ClickPlayBtn();
}else if(v.getId() == R.id.iv_full){
if(mListener != null) mListener.ClickFullButton();
}else if(v.getId() == R.id.videoview){
if(mListener != null) mListener.ClickVideoView();
}
}
public interface VideoPlayStatusChangedListener{
void ClickVideoView();
void ClickFullButton();
void ClickPlayBtn();
/**
* video
*/
void VideoLoadSuccess();
void VideoLoadError();
void VideoComplete();
void BufferedUpdate(int position);
}
MediaPlayer + TextureView的配合,重点要熟悉MediaPlayer 和TextureView的生命周期,数据流需要将视频信息输出到TextureView,那么只有在TextureView准备好之后,才可以将数据信息上传到TextureView,也就是需要执行TextureView的
onSurfaceTextureAvailable
方法,在这个方法中,将MediaPlayer和Surface绑定,开始加载视频数据,在视频播放的完成或者失败的情况下,会走onCompletion
和onError
方法;
在此类中,主要是做了事件的触发回调,主要的事件处理,在VideoController中实现。
/**
* 处理事件回调
*/
public class VideoController implements
CustomVideoView.VideoPlayStatusChangedListener, FullScreenDialog.BackToSmallListener {
private Context mContext;
//装载VideoView的ViewGroup
private ViewGroup mParentView;
//加载视频的Url地址
private String videoUrl;
//监听
private VideoSlotListener mListener;
//播放器
private CustomVideoView videoView;
//当前的进度
private int mCurrentPosition;
public VideoController(String instance,VideoSlotListener listener){
this.videoUrl = instance;
this.mListener = listener;
mParentView = listener.getParentView();
this.mContext = mParentView.getContext();
//加载VideoView
initVideoView();
}
private void initVideoView() {
if(videoView == null){
videoView = new CustomVideoView(mContext);
videoView.setDataSource(videoUrl);
//必须要设置,否则点击无效
videoView.setListener(this);
}
RelativeLayout relativeLayout = new RelativeLayout(mContext);
relativeLayout.setBackgroundResource(android.R.color.black);
relativeLayout.setLayoutParams(videoView.getLayoutParams());
mParentView.addView(relativeLayout);
mParentView.addView(videoView);
}
@Override
public void ClickVideoView() {
//点击到了VideoView
if(videoView.isPlaying()){
//如果当前正在播放
videoView.seekAndPause(mCurrentPosition);
}else{
videoView.resume();
}
}
//进入全屏模式
@Override
public void ClickFullButton() {
//首先从当前父容器移除
mParentView.removeView(videoView);
//将全屏按钮隐藏
videoView.showFullBtn(false);
//创建全屏的View
FullScreenDialog dialog = new FullScreenDialog(mContext,
videoView,videoUrl);
dialog.setListener(this);
dialog.show();
}
@Override
public void ClickPlayBtn() {
videoView.resume();
}
@Override
public void VideoLoadSuccess() {
}
@Override
public void VideoLoadError() {
}
@Override
public void VideoComplete() {
}
@Override
public void BufferedUpdate(int position) {
mCurrentPosition = position;
}
@Override
public void backToSmallScreen(int position) {
//将VideoView重新添加到容器中
mParentView.addView(videoView);
//显示全屏按钮
videoView.showFullBtn(true);
//如果当前是播放状态
if(videoView.isPlaying()){
videoView.resume();
}else{
//
videoView.seekAndPause(position);
}
//重新设置监听
videoView.setListener(this);
}
@Override
public void videoComplete() {
}
public interface VideoSlotListener{
ViewGroup getParentView();
void videoLoadSuccess();
void videoLoadError();
void videoLoadComplete();
}
}
在使用视频播放器的时候,需要将该播放器加入父容器中,这个小屏播放的模式,在该类中,一个最重要的逻辑处理是大小屏的处理,在点击全屏按钮的时候,需要切换到全屏按钮。
在点击全屏按钮之后,需要弹出一个全屏的Dialog,这个Dialog属于该播放器的新的父容器(播放器可复用),所以在进入全屏播放的时候,需要先从当前父容器中移除。
在移除之后,需要将该播放器添加到新的父容器。
public class FullScreenDialog extends Dialog implements CustomVideoView.VideoPlayStatusChangedListener{
private CustomVideoView videoView;
private String videoUrl;
//父容器
private RelativeLayout mParentView;
//返回按钮
private ImageView mBackBtn;
//监听器
private BackToSmallListener mListener;
//当前播放的进度
private int mCurrentPosition;
public FullScreenDialog(@NonNull Context context,
CustomVideoView videoView,String instance) {
super(context,android.R.style.Theme_Material_Dialog);
Window window = getWindow();
window.setGravity(Gravity.CENTER);
window.getDecorView().setBackgroundResource(android.R.color.black);
window.setLayout(RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT);
this.videoView = videoView;
this.videoUrl = instance;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.layout_dialog_view);
initDialog();
}
private void initDialog() {
mParentView = findViewById(R.id.root_view);
mBackBtn = findViewById(R.id.iv_close);
mBackBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//进入小屏模式
backToSmallScreen();
}
});
videoView.setListener(this);
mParentView.addView(videoView);
}
private void backToSmallScreen() {
dismiss();
if(mListener != null){
mListener.backToSmallScreen(mCurrentPosition);
}
}
@Override
public void dismiss() {
//从当前容器移除
mParentView.removeView(videoView);
super.dismiss();
}
public void setListener(BackToSmallListener mListener) {
this.mListener = mListener;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (!hasFocus){
//没有获取焦点
mCurrentPosition = videoView.getCurrentPosition();
videoView.seekAndPause(mCurrentPosition);
}else{
//当获取焦点后
videoView.resume();
}
}
@Override
public void ClickVideoView() {
if(videoView.isPlaying()){
videoView.seekAndPause(mCurrentPosition);
}else{
videoView.showFullBtn(false);
videoView.resume();
}
}
@Override
public void ClickFullButton() {
}
@Override
public void ClickPlayBtn() {
videoView.showFullBtn(false);
videoView.resume();
}
@Override
public void VideoLoadSuccess() {
}
@Override
public void VideoLoadError() {
}
@Override
public void onBackPressed() {
//返回按钮也是返回小屏幕
backToSmallScreen();
super.onBackPressed();
}
/**
* 视频播放完毕
*/
@Override
public void VideoComplete() {
videoView.showFullBtn(true);
//返回小屏模式
backToSmallScreen();
}
@Override
public void BufferedUpdate(int position) {
mCurrentPosition = position;
}
public interface BackToSmallListener{
void backToSmallScreen(int position);
void videoComplete();
}
}
在全屏进入小屏的时候,同样也需要将播放器从Dialog中移除,将其放入到之前的父容器中,因为当前是Dialog 所以需要在dismiss
方法中,从当前父容器移除。
关于一些状态的保持和切换,都是一些细节的处理,在代码中也有详细的解释,目前只是能够稳定地播放视频,视频播放器的其他功能,包括TextureView可以添加动画等等功能,在后续的章节中,将会继续完善该播放器的功能。