一步步自定义视频播放器——TextureView+MediaPlayer自定义视频播放器

本篇参考封装一个视频播放器,原文已经写的非常棒了,本篇加入了个人对其内容的理解。秉承不重复造轮子的良好理念,接下来开始拆解轮子。内容非常多,我都差点放弃写,有耐心的请往下看

github上非常棒的视频相关开源项目有:
最炫的哔哩哔哩的ijkplayer
JiaoziVideoPlayer 基于ijkplayer
GSYVideoPlayer 基于ijkplayer

结合前三篇内容,我们已经对MediaPlayer,SurfaceView,TextureView有了基本了解。相比SurfaceView,TextureView的类View特性,使其更加灵活。因此,我们选择TextureView结合MediaPlayer的方式来自定义视频播放器。只要你静下心来,跟着源码一步步敲,慢慢理解,你就会发现原来自定义视频播放器并不难。

核心思想

类似MVC模式

  • NiceVideoPlayer:自定义播放界面,其主要是MediaPlayer和TextureView结合,负责视频数据解析和显示。
  • NiceVideoController:控制界面,主要是控制暂停,播放,亮度,音量等
  • INiceVideoPlayer:接口,定义了视频播放的相关方法,例如播放,暂停,获取当前播放状态等。

INiceVideoPlayer接口

定义接口前我们先思考下我们需要VideoPlayer实现什么功能,给VideoContronller提供哪些方法。

  1. 播放状态,根据前面的知识我们知道,MediaPlayer有多种状态,我们需要判断MediaPlayer的状态
1
2
3
4
5
6
7
8
9
10
11
12
/******************************
* 以下方判断播放器当前的播放状态
******************************/
boolean isIdle();//是否空闲
boolean isPreparing();
boolean isPrepared();
boolean isBufferingPlaying();
boolean isBufferingPaused();
boolean isPlaying();
boolean isPaused();
boolean isError();
boolean isCompleted();
  1. 播放模式,基本的播放模式有正常模式,全屏模式,以及小窗口模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*************************
* 播放器模式
*************************/
boolean isFullScreen();
boolean isTinyWindow();
boolean isNormal();

/**
* 进入全屏模式
*/
void enterFullScreen();

/**
* 退出全屏模式
* @return
*/
boolean exitFullScreen();

/**
* 进入小窗口模式
*/
void enterTinyWindow();

/**
* 退出小窗口模式
* @return
*/
boolean exitTinyWindow();
  1. 播放控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 开始播放
*/
void start();

/**
* 从指定位置开始播放
*
* @param position 播放位置
*/
void start(long position);

/**
* 重新播放,播放器被暂停,播放错误,播放完成后需要调用这个方法重新播放
*/
void restart();

/**
* 暂停播放
*/
void pause();

/**
* 释放videoPlayer
*/
void releasePlayer();

/**
* 释放
*/
void release();

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
public interface INiceVideoPlayer {

/**
* 设置视频的url,以及headers
*
* @param url 视频地址,可以是本地,也可以是网络
* @param headers 请求header
*/
void setUp(String url, Map<String, String> headers);

/***************
* 播放控制
****************/

/**
* 开始播放
*/
void start();

/**
* 从指定位置开始播放
*
* @param position 播放位置
*/
void start(long position);

/**
* 重新播放,播放器被暂停,播放错误,播放完成后需要调用这个方法重新播放
*/
void restart();

/**
* 暂停播放
*/
void pause();

/**
* 释放videoPlayer
*/
void releasePlayer();

/**
* 释放
*/
void release();


/**
* seek到指定的位置继续播放
*
* @param pos
*/
void seekTo(long pos);

/**
* 开始播放时,是否重上一次的位置继续播放
*
* @param continueFromLastPosition 上次播放位置
*/
void continueFromLastPostion(boolean continueFromLastPosition);

/**
* 获取当前的播放位置
*/
long getCurrentPostion();

/**
* 获取视频缓冲百分比
*/
int getBufferPercentage();

/**
* 设置音量
*
* @param volume 音量值
*/
void setVolume(int volume);

/**
* 获取最大音量
*
* @return
*/
int getMaxVolume();

/**
* 获取当前的音量
*
* @return
*/
int getVolume();

/**
* 获取总时长
*
* @return
*/
long getDuration();


/******************************
* 播放器当前的播放状态
******************************/
boolean isIdle();//是否空闲

boolean isPreparing();

boolean isPrepared();

boolean isBufferingPlaying();

boolean isBufferingPaused();

boolean isPlaying();

boolean isPaused();

boolean isError();

boolean isCompleted();


/*************************
* 播放器模式
*************************/
boolean isFullScreen();

boolean isTinyWindow();

boolean isNormal();

/**
* 进入全屏模式
*/
void enterFullScreen();

/**
* 退出全屏模式
*
* @return
*/
boolean exitFullScreen();

/**
* 进入小窗口模式
*/
void enterTinyWindow();

/**
* 退出小窗口模式
*
* @return
*/
boolean exitTinyWindow();
}

NiceVideoPlayer

定义播放状态,我们知道MediaPlayer在初始化和reset处于 Idle状态即STATE_IDLE。

播放视频时,MediaPlayer准备就绪(Prepared)后没有马上进入播放状态,中间有一个时间延迟时间段,然后开始渲染图像。所以将Prepared——>“开始渲染”中间这个时间段定义为STATE_PREPARED。

如果是播放网络视频,在播放过程中,缓冲区数据不足时MediaPlayer内部会停留在某一帧画面以进行缓冲。正在缓冲时,MediaPlayer可能是在正在播放也可能是暂停状态,因为在缓冲时如果用户主动点击了暂停,就是处于STATE_BUFFERING_PAUSED,所以缓冲有STATE_BUFFERING_PLAYING和STATE_BUFFERING_PAUSED两种状态,缓冲结束后,恢复播放或暂停。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/****************
* 播放状态
****************/
/**
* 播放错误
*/
public static final int STATE_ERROR = -1;
/**
* 播放未开始
*/
public static final int STATE_IDLE = 0;
/**
* 播放准备中
*/
public static final int STATE_PREPARING = 1;
/**
* 播放准备就绪
*/
public static final int STATE_PREPARED = 2;
/**
* 正在播放
*/
public static final int STATE_PLAYING = 3;
/**
* 暂停播放
*/
public static final int STATE_PAUSED = 4;
/**
* 正在缓冲,播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复
*/
public static final int STATE_BUFFERING_PLAYING =5;
/**
* 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
**/
public static final int STATE_BUFFERING_PAUSED = 6;
/**
* 播放完成
*/
public static final int STATE_COMPLETED = 7;

播放模式普通模式,全屏模式,小屏模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*************
* 播放模式
*************/
/**
* 普通模式
*/
public static final int MODE_NORMAL = 10;
/**
* 全屏模式
*/
public static final int MODE_FULL_SCREEN = 11;
/**
* 小窗口模式
*/
public static final int MODE_TINY_WINDOW = 12;

思路

  1. 让NiceVideoPlayer继承Framelayout,因为我们的内容是嵌套的
  2. this.addview(Contanier) 创建一个FragmeLayout的容器,用来装载,管理TextureView
  3. Container界面添加TextureView界面的基础上,再添加Controller界面,这个界面中包含了音量,亮度,控制条等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public NiceVideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();
}

private void init() {
mContainer = new FrameLayout(mContext);//创建容器布局
mContainer.setBackgroundColor(Color.BLACK);//设置背景色为黑色
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);//设置铺满全屏
this.addView(mContainer, params);
}

@Override
public void setUp(String url, Map<String, String> headers) {
mUrl = url;
mHeaders = headers;
}

//设置控制音量,亮度,控制条等界面
public void setController(NiceVideoController controller) {
//清除
mContainer.removeView(mController);
mController = controller;
mController.reset();//重置

mController.setNiceVideoPlayer(this);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
//添加控制布局
mContainer.addView(mController, params);
}

/**
* 将TextureView添加到Container中
*/
private void addTextureView() {

mContainer.removeView(mTextureView);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mContainer.addView(mTextureView, 0, params);
}

播放开始

在start() 方法中,初始化我们的音频,MediaPlayer和TextureView,并建立监听,setSurfaceTextureListener(this)。当onSurfaceTextureAvailable时,MediaPlayer进入准备状态,直到准备就绪便播放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@Override
public void start() {
//只有IDLE状态下才能开始
if (mCurrentState == STATE_IDLE) {
//初始化
initAudioManager();
initMediaPlayer();
initTextureView();
} else {
LogUtils.d("NiceVideoPlayer只有在mCurrentState==STATE_IDLE时才能调用");
}
}

@Override
public void start(long position) {
skipToPosition = position;
start();
}

/**
* 初始化音频管理器
*/
private void initAudioManager() {
if (mAudioManager == null)
mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
//获得音频焦点
mAudioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
}

/**
* 初始化MediaPlayer
*/
private void initMediaPlayer() {
if (mMediaplayer == null) {
mMediaplayer = new MediaPlayer();
mMediaplayer.reset();
mMediaplayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
}

/**
* 初始化TextureView
*/
private void initTextureView() {
if (mTextureView == null) {
mTextureView = new TextureView(mContext);
mTextureView.setSurfaceTextureListener(this);
}
addTextureView();//将TextureView添加到Container中
}

/**
* 将TextureView添加到Container中
*/
private void addTextureView() {

mContainer.removeView(mTextureView);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mContainer.addView(mContainer, 0, params);
}

/*********************
* SurfaceTexture监听
*********************/

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if (mSurfaceTexture == null) {
mSurfaceTexture = surface;
openMediaPlayer();
} else {
mTextureView.setSurfaceTexture(mSurfaceTexture);
}
}

/**
* 添加相关监听,进入准备
*/
private void openMediaPlayer() {

try {
mContainer.setKeepScreenOn(true);//设置屏幕常亮

//设置数据
mMediaplayer.setDataSource(mContext.getApplicationContext(), Uri.parse(mUrl), mHeaders);
if (mSurface == null) {
mSurface = new Surface(mSurfaceTexture);
}
//设置渲染
mMediaplayer
.setSurface(mSurface);
mMediaplayer.prepareAsync();//准备
mCurrentState = STATE_PREPARING;
mController.onPlayStateChanged(mCurrentState);
LogUtils.d("STATE_PREPARING");
} catch (IOException e) {
e.printStackTrace();
LogUtils.d("打开播放器发生错误");
}

//设置监听
mMediaplayer.setOnPreparedListener(this);
mMediaplayer.setOnVideoSizeChangedListener(this);
mMediaplayer.setOnCompletionListener(this);
mMediaplayer.setOnErrorListener(this);
mMediaplayer.setOnInfoListener(this);
mMediaplayer.setOnBufferingUpdateListener(this);
}

//准备监听
@Override
public void onPrepared(MediaPlayer mp) {
mCurrentState = STATE_PREPARED;
mController.onPlayStateChanged(mCurrentState);
LogUtils.d("STATE_PREPARED");
mp.start();//开始播放
}

释放资源

视频播放是很消耗资源的,一定要记得回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public void releasePlayer() {
if (mAudioManager != null) {
mAudioManager.abandonAudioFocus(null);//释放焦点
mAudioManager = null;
}
if (mMediaplayer != null) {
mMediaplayer.release();
mMediaplayer = null;
}

mContainer.removeView(mTextureView);
if (mSurface != null) {
mSurface.release();
mSurface = null;
}
if (mSurfaceTexture != null) {
mSurfaceTexture.release();
mSurfaceTexture = null;
}
mCurrentState = STATE_IDLE;
}

//释放
@Override
public void release() {
//退出全屏
if (isFullScreen()) {
exitFullScreen();
}
//退出小窗口
if (isTinyWindow()) {
exitTinyWindow();
}
mCurrentMode = MODE_NORMAL;
//释放播放器
releasePlayer();
//释放控制器
if (mController != null) {
mController.reset();
}
Runtime.getRuntime().gc();//回收
}

窗口模式变化

切换横竖屏时为了避免Activity重新走生命周期,需要在Manifest.xml的activity标签下添加如下配置:

1
android:configChanges="orientation|keyboardHidden|screenSize"

每个Activity里面都有一个android.R.content,它是一个FrameLayout,里面包含了我们setContentView的所有控件。既然它是一个FrameLayout,我们就可以将它作为全屏和小窗口的目标视图。

我们把从当前视图移除的mContainer重新添加到android.R.content中,并且设置成横屏。这个时候还需要注意android.R.content是不包括ActionBar和状态栏的,所以要将Activity设置成全屏模式,同时隐藏ActionBar。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//全屏模式
@Override
public void enterFullScreen() {
if (mCurrentMode == MODE_FULL_SCREEN) return;
//隐藏状态栏
NiceUtil.hideActionBar(mContext);
//context转换为Activity
Activity activity = NiceUtil.scanForActivity(mContext);
if (activity != null) {//转换为横屏
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

ViewGroup contentView = activity.findViewById(android.R.id.content);
//移除布局
if (mCurrentMode == MODE_TINY_WINDOW) {
contentView.removeView(mContainer);
} else {
this.removeView(mContainer);
}
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
//将布局添加到ContentView
contentView.addView(mContainer,params);
mCurrentMode = MODE_FULL_SCREEN;
mController.onPlayModeChanged(mCurrentMode);
LogUtils.d("MODE_FULL_SCREEN");
}
}

退出全屏也就很简单了,将mContainer从android.R.content中移除,重新添加到当前视图,并恢复ActionBar、清除全屏模式就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//退出全屏
@Override
public boolean exitFullScreen() {
if (mCurrentMode == MODE_FULL_SCREEN) {
//显示状态栏
NiceUtil.showActionBar(mContext);
Activity activity = NiceUtil.scanForActivity(mContext);
if (activity != null) {
//切换竖屏
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
ViewGroup contentView = activity.findViewById(android.R.id.content);
contentView.removeView(mContainer);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(mContainer, params);
mCurrentMode = MODE_NORMAL;
mController.onPlayModeChanged(mCurrentMode);
LogUtils.d("MODE_NORMAL");
return true;
}
}
return false;
}

进入小窗口播放和退出小窗口的实现原理就和全屏功能一样了,只需要修改它的宽高参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//进入小窗口
@Override
public void enterTinyWindow() {
if (mCurrentMode == MODE_TINY_WINDOW) return;
this.removeView(mContainer);
Activity activity = NiceUtil.scanForActivity(mContext);
if (activity != null) {
ViewGroup contentView = activity.findViewById(android.R.id.content);
//设置小窗口的布局大小,宽度为屏幕宽度的60%,长宽比默认为16:9,右边距、下边距为8dp。
//height = 9/16 * 0.6width
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
//设置位置
params.gravity = Gravity.BOTTOM | Gravity.RIGHT;
//设置边距
params.rightMargin = NiceUtil.dp2px(mContext, 8f);
params.bottomMargin = NiceUtil.dp2px(mContext, 8f);
contentView.addView(mContainer,params);
mCurrentMode = MODE_TINY_WINDOW;
mController.onPlayModeChanged(mCurrentMode);
LogUtils.d("MODE_TINY_WINDOW");
}
}

移除小窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//退出小窗口
@Override
public boolean exitTinyWindow() {
if (mCurrentMode == MODE_TINY_WINDOW) {
Activity activity = NiceUtil.scanForActivity(mContext);
if (activity != null) {
ViewGroup contentView = activity.findViewById(android.R.id.content);
contentView.removeView(mContainer);
LayoutParams params = (LayoutParams) new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
this.addView(mContainer, params);
mCurrentMode = MODE_NORMAL;
mController.onPlayModeChanged(mCurrentMode);
LogUtils.d("MODE_NORMAL");
return true;
}
}
return false;
}

注意
当mContainer移除重新添加后,mContainer及其内部的mTextureView和mController都会重绘,mTextureView重绘后,会重新new一个SurfaceTexture,并重新回调onSurfaceTextureAvailable方法,这样mTextureView的数据通道SurfaceTexture发生了变化,但是mMediaPlayer还是持有原先的mSurfaceTexut,所以在切换全屏之前要保存之前的mSufaceTexture,当切换到全屏后重新调用onSurfaceTextureAvailable时,将之前的mSufaceTexture重新设置给mTexutureView。这样就保证了切换时视频播放的无缝衔接。

1
2
3
4
5
6
7
8
9
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
if (mSurfaceTexture == null) {
mSurfaceTexture = surfaceTexture;
openMediaPlayer();
} else {
mTextureView.setSurfaceTexture(mSurfaceTexture);
}
}

NiceVideoController

这个控制界面绘制有底部进度条,音量控制,亮度控制,播放暂停按钮等。我们通过在NiceVideoPlayer中setController(NiceVideoPlayer)将NiceVideoPlayer传递到Controller中。

1
2
3
4
5
6
/**
* @param niceVideoPlayer 得到NiceVideoPlayer
*/
protected void setNiceVideoPlayer(INiceVideoPlayer niceVideoPlayer) {
mNiceVideoPlayer = niceVideoPlayer;
}

NiceVideoController是一个抽象类,这里我们定义了控制界面要实现的抽象方法,且实现了音量,亮度的方法。

我们另左半屏控制亮度,右半屏控制音量,横向滑动控制进度,进度的变化调用了计时器,间隔一定时间来更新我们的进度信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
    /**
* 控制进度条,声音,亮度
*
* @param event 触摸事件
*/

@Override
public boolean onTouch(View v, MotionEvent event) {
//只在全屏时可拖动位置、亮度和声音
if (!mNiceVideoPlayer.isFullScreen()) {
return false;
}
// 只有在播放、暂停、缓冲的时候能够拖动改变位置、亮度和声音
if (mNiceVideoPlayer.isIdle()
|| mNiceVideoPlayer.isError()
|| mNiceVideoPlayer.isPreparing()
|| mNiceVideoPlayer.isPrepared()
|| mNiceVideoPlayer.isCompleted()) {
hideChangePosition();
hideChangeBrightness();
hideChangeVolume();
return false;
}
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下时记录位置
mDownX = x;
mDownY = y;
mNeedChangePosition = false;
mNeedChangeBrightness = false;
mNeedChangeVolume = false;
break;
case MotionEvent.ACTION_MOVE://移动
float deltaX = x - mDownX;
float deltaY = y - mDownY;
float absDeltaX = Math.abs(deltaX);
float absDeltaY = Math.abs(deltaY);
// 只有在播放、暂停、缓冲的时候能够拖动改变位置、亮度和声音
if (!mNeedChangePosition && !mNeedChangeVolume && !mNeedChangeBrightness) {
// 只有在播放、暂停、缓冲的时候能够拖动改变位置、亮度和声音
if (absDeltaX >= THRESHOLD) {
cancelUpdateProgressTimer();
mNeedChangePosition = true;
mGestureDownPosition = mNiceVideoPlayer.getCurrentPosition();
} else if (absDeltaY >= THRESHOLD) {
if (mDownX < getWidth() * 0.5f) {
// 左侧改变亮度
mNeedChangeBrightness = true;
if (NiceUtil.scanForActivity(mContext) != null) {
mGestureDownBrightness = NiceUtil.scanForActivity(mContext)
.getWindow().getAttributes().screenBrightness;
}
} else {
// 右侧改变声音
mNeedChangeVolume = true;
mGestureDownVolume = mNiceVideoPlayer.getVolume();
}
}
}

if (mNeedChangePosition) {
long duration = mNiceVideoPlayer.getDuration();
//当前距离+屏幕滑动距离
long toPosition = (long) (mGestureDownPosition + duration * deltaX / getWidth());
mNewPosition = Math.max(0, Math.min(duration, toPosition));
int newPositionProgress = (int) (100f * mNewPosition / duration);
showChangePosition(duration, newPositionProgress);
}
//亮度 0-1
if (mNeedChangeBrightness) {
deltaY = -deltaY;//由于是向下滑时,最终距离比开始距离大,为正数
float deltaBrightness = 3 * deltaY / getHeight();//滑动的距离除以屏幕高度的比例,可以乘以一定的倍率,来加大滑动效果。
float newBrightness = mGestureDownBrightness + deltaBrightness;
newBrightness = Math.max(0, Math.min(newBrightness, 1));
Activity activity = NiceUtil.scanForActivity(mContext);
if (activity != null) {
WindowManager.LayoutParams params = activity.getWindow().getAttributes();
params.screenBrightness = newBrightness;
//设置当前窗口亮度值
activity.getWindow().setAttributes(params);
int newBrightnessProgress = (int) (newBrightness * 100f);
showChangeBrightness(newBrightnessProgress);
}
}
if (mNeedChangeVolume) {
deltaY = -deltaY;
int maxVolume = mNiceVideoPlayer.getMaxVolume();
int deltaVolume = (int) (deltaY * maxVolume * 3 / getHeight());
int newVolume = mGestureDownVolume + deltaVolume;
newVolume = Math.max(0, Math.min(maxVolume, newVolume));
//设置volume
mNiceVideoPlayer.setVolume(newVolume);
int newVolumeProgress = (int) (100f * newVolume / maxVolume);
showChangeVolume(newVolumeProgress);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (mNeedChangePosition) {
mNiceVideoPlayer.seekTo(mNewPosition);
hideChangePosition();
startUpdateProgressTimer();
return true;
}
if (mNeedChangeVolume) {
hideChangeVolume();
return true;
}
if (mNeedChangeBrightness) {
hideChangeBrightness();
return true;
}
}
return false;
}

我们通过Timer和TimerTask每隔一段时间执行一次进度更新,这里介绍了Timer和TimerTask的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 开启进度更新计时器
*/
protected void startUpdateProgressTimer() {
cancelUpdateProgressTimer();
if (mUpdateProgressTimer == null) {
mUpdateProgressTimer = new Timer();
}
if (mUpdateProgressTimerTask == null) {
mUpdateProgressTimerTask = new TimerTask() {
@Override
public void run() {
//主线程中更新
NiceVideoController.this.post(new Runnable() {
@Override
public void run() {
updateProgress();
}
});
}
};
}
//开启 mUpdateProgressTimer.schedule(mUpdateProgressTimerTask, 0, 1000);
}

/**
* 取消更新进度的计时器
*/
protected void cancelUpdateProgressTimer() {
if (mUpdateProgressTimer != null) {
mUpdateProgressTimer.cancel();
mUpdateProgressTimer = null;
}

if (mUpdateProgressTimerTask != null) {
mUpdateProgressTimerTask.cancel();
mUpdateProgressTimerTask = null;
}
}

TxVideoPlayerController

真正的控制界面类,继承我们的NiceVideoController。在这里我们添加上控制界面。我们可以继承NiceVideoController来自定义想要的控制界面效果。

1
2
3
4
 private void init() {
//初始化布局
LayoutInflater.from(mContext).inflate(R.layout.tx_video_palyer_controller, this, true);
}

在这里我们处理MediaPlayer各种状态中相应的界面显示效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Override
protected void onPlayStateChanged(int playState) {
switch (playState) {
case NiceVideoPlayer.STATE_IDLE:
break;
case NiceVideoPlayer.STATE_PREPARING://准备中
mImage.setVisibility(GONE);//底图
mLoading.setVisibility(VISIBLE);//加载图
mLoadText.setText("正在准备...");
mError.setVisibility(GONE);
mCompleted.setVisibility(GONE);
mTop.setVisibility(GONE);
mBottom.setVisibility(GONE);
mCenterStart.setVisibility(GONE);
mLength.setVisibility(GONE);
break;
case NiceVideoPlayer.STATE_PREPARED://就绪状态
startUpdateProgressTimer();//开始更新进度计时器
break;
case NiceVideoPlayer.STATE_PLAYING://播放状态
mLoading.setVisibility(GONE);
mRestartPause.setImageResource(R.drawable.ic_player_pause);
startDismissTopBottomTimer();
break;
case NiceVideoPlayer.STATE_PAUSED:
mLoading.setVisibility(GONE);
mRestartPause.setImageResource(R.drawable.ic_player_start);
cancelDismissTopBottomTimer();
break;
case NiceVideoPlayer.STATE_BUFFERING_PLAYING:
mLoading.setVisibility(VISIBLE);
mRestartPause.setImageResource(R.drawable.ic_player_pause);
mLoadText.setText("正在缓冲中...");
startDismissTopBottomTimer();
break;
case NiceVideoPlayer.STATE_BUFFERING_PAUSED:
//缓冲状态
mRestartPause.setImageResource(R.drawable.ic_player_start);
mLoadText.setText("正在缓冲中...");
cancelDismissTopBottomTimer();
break;
case NiceVideoPlayer.STATE_ERROR:
cancelUpdateProgressTimer();//取消进度条更新
setTopBottomVisible(false);
mTop.setVisibility(VISIBLE);
mError.setVisibility(VISIBLE);
break;
case NiceVideoPlayer.STATE_COMPLETED:
cancelUpdateProgressTimer();
setTopBottomVisible(false);
mImage.setVisibility(VISIBLE);
mCompleted.setVisibility(VISIBLE);
break;
}
}

模式变化后,显示相应的界面效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
protected void onPlayModeChanged(int playMode) {
switch (playMode) {
case NiceVideoPlayer.MODE_NORMAL:
mBack.setVisibility(View.GONE);
mFullScreen.setImageResource(R.drawable.ic_player_enlarge);
mFullScreen.setVisibility(View.VISIBLE);
mClarity.setVisibility(View.GONE);
mBatteryTime.setVisibility(View.GONE);
if (hasRegisterBatteryReceiver) {
mContext.unregisterReceiver(mBatterReceiver);
hasRegisterBatteryReceiver = false;
}
break;
case NiceVideoPlayer.MODE_FULL_SCREEN:
mBack.setVisibility(View.VISIBLE);
mFullScreen.setVisibility(View.GONE);
mFullScreen.setImageResource(R.drawable.ic_player_shrink);
mBatteryTime.setVisibility(View.VISIBLE);
if (!hasRegisterBatteryReceiver) {//电池监听变化
mContext.registerReceiver(mBatterReceiver,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
hasRegisterBatteryReceiver = true;
}
break;
case NiceVideoPlayer.MODE_TINY_WINDOW:
mBack.setVisibility(View.VISIBLE);
mClarity.setVisibility(View.GONE);
break;
}

}

通过CountDownTimer来倒计时来自动消失状态控制栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 开启top bottom 自动消失的timer
*/
private void startDismissTopBottomTimer() {
cancelDismissTopBottomTimer();
if (mDismissTopBottomCountDownTimer == null) {
mDismissTopBottomCountDownTimer = new CountDownTimer(8000, 800) {
@Override
public void onTick(long millisUntilFinished) {
}

@Override
public void onFinish() {
//倒计时结束,隐藏底部栏
setTopBottomVisible(false);
}
};
}
mDismissTopBottomCountDownTimer.start();
}

/**
* 取消top,bottom自动消失的timer
*/
private void cancelDismissTopBottomTimer() {
if (mDismissTopBottomCountDownTimer != null) {
mDismissTopBottomCountDownTimer.cancel();
}
}

/**
* 设置top、bottom的显示和隐藏
*
* @param visible true显示,false隐藏.
*/
private void setTopBottomVisible(boolean visible) {
mTop.setVisibility(visible ? VISIBLE : GONE);
mBottom.setVisibility(visible ? VISIBLE : GONE);
if (visible) {
//在非暂停,非缓冲暂停的状态,顶部和底部的状态栏自动消失
if (!mNiceVideoPlayer.isPaused() && !mNiceVideoPlayer.isBufferingPaused()) {
startDismissTopBottomTimer();
}
} else {
cancelDismissTopBottomTimer();//取消自动倒计时
}
}

显而易见,在这个类中我们主要做的事就是决定什么情况下显示哪些内容。

NiceVideoPlayerManager

在这个视屏播放器的管理类中,我们通过setCurrentVideoPlayer();来管理NiceVideoPlayer,控制他的销毁,播放。

在NiceVideoPlayer中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void start() {
NiceVideoPlayerManager.instance().setCurrentNiceVideoPlayer(this);

//只有IDLE状态下才能开始
if (mCurrentState == STATE_IDLE) {
//初始化
initAudioManager();
initMediaPlayer();
initTextureView();
} else {
LogUtils.d("NiceVideoPlayer只有在mCurrentState==STATE_IDLE时才能调用");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

public class NiceVideoPlayerManager {
private static NiceVideoPlayerManager sInstance;
private NiceVideoPlayer mVideoPlayer;

private NiceVideoPlayerManager() {
}

//单例模式
public static NiceVideoPlayerManager instance() {
if (sInstance == null) {
synchronized (NiceVideoPlayerManager.class) {
if (sInstance == null) {
sInstance = new NiceVideoPlayerManager();
}
}
}
return sInstance;
}

/**
* 获取当前的NiceVideoPlayer
*
* @return NiceVideoPlayer
*/
public NiceVideoPlayer getCurrentNiceVideoPlayer() {
return mVideoPlayer;
}

/**
* 设置当前的NiceVideoPlayer
*/
public void setCurrentNiceVideoPlayer(NiceVideoPlayer niceVideoPlayer) {
if (mVideoPlayer != niceVideoPlayer) {
releaseNiceVideoPlayer();
mVideoPlayer = niceVideoPlayer;
}
}

/**
* 暂停
*/
public void suspendNiceVideoPlayer() {
if (mVideoPlayer != null && (mVideoPlayer.isPlaying() || mVideoPlayer.isBufferingPlaying())) {
mVideoPlayer.pause();
}
}

/**
* 恢复
*/
public void resumeNiceVideoPlayer(){
if (mVideoPlayer != null && (mVideoPlayer.isPaused() || mVideoPlayer.isBufferingPaused())) {
mVideoPlayer.restart();
}
}


/**
* 释放NiceVideoPlayer
*/
public void releaseNiceVideoPlayer() {
if (mVideoPlayer != null) {
mVideoPlayer.release();
mVideoPlayer = null;
}
}

/**
* 按返回键时
*/
public boolean onBackPressd() {
if (mVideoPlayer != null) {
if (mVideoPlayer.isFullScreen()) {
return mVideoPlayer.exitFullScreen();
} else if (mVideoPlayer.isTinyWindow()) {
return mVideoPlayer.exitTinyWindow();
}
}
return false;
}
}

使用方式为:
在对应视频界面所在的Activity的Manifest.xml中需要添加如下配置:

1
2

android:configChanges="orientation|keyboardHidden|screenSize"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class XXXActivity extends AppCompatActivity {
...
@Override
public void onBackPressed() {
// 在全屏或者小窗口时按返回键要先退出全屏或小窗口,
// 所以在Activity中onBackPress要交给NiceVideoPlayer先处理。
if (NiceVideoPlayerManager.instance().onBackPressd()) return;
super.onBackPressed();
}
...
}

同时在Fragment中的onStop方法中释放播放器:

public class XXXFragenment extends Fragment {
...
@Override
public void onStop() {
super.onStop();
NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
}
...
}

在布局文件中添加

1
2
3
4
<XXXX.NiceVideoPlayer
android:id="@+id/videoplayer"
android:layout_width="match_parent"
android:layout_height="200dp"/>

代码中使用方式

1
2
3
4
TxVideoPlayerController controller = new TxVideoPlayerController(mContext);
mVideoPlayer = (NiceVideoPlayer)findViewById(R.id.videoplayer);
mVideoPlayer.setController(controller);
controller.setUrl(url);

小例子

注意添加权限

1
2
3
4
5
6
   <uses-permission android:name="android.permission.INTERNET" />

<activity
android:name=".NormalActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="portrait" />

正常显示方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class NormalActivity extends AppCompatActivity {
private NiceVideoPlayer niceVideoPlayer;
public String url = "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-33-30.mp4";
public String imgUrl = "http://tanzi27niu.cdsb.mobi/wps/wp-content/uploads/2017/05/2017-05-17_17-30-43.jpg";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_normal_layout);
niceVideoPlayer = findViewById(R.id.nice_video_player);

TxVideoPlayerController controller = new TxVideoPlayerController(this);
niceVideoPlayer.setController(controller);
controller.setUrl(url);
controller.setTitle("呵呵呵");
}

@Override
public void onBackPressed() {
if (NiceVideoPlayerManager.instance().onBackPressd()) return;
super.onBackPressed();
}

@Override
protected void onStop() {
super.onStop();
NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
}
}

布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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"
tools:context="com.example.com.simplenicevideoplayer.MainActivity">

<com.example.com.videoplayer.NiceVideoPlayer
android:id="@+id/nice_video_player"
android:layout_width="match_parent"
android:layout_height="200dp"/>

</LinearLayout>

列表使用方式

注意:添加回收

1
2
3
4
5
6
7
8
9
10
11
recyclerView.setRecyclerListener(new RecyclerView.RecyclerListener() {
@Override
public void onViewRecycled(RecyclerView.ViewHolder holder) {
if (holder instanceof TestAdapter.ViewHolder) {
NiceVideoPlayer niceVideoPlayer = ((TestAdapter.ViewHolder) holder).niceVideoPlayer;
if (niceVideoPlayer == NiceVideoPlayerManager.instance().getCurrentNiceVideoPlayer()) {
NiceVideoPlayerManager.instance().releaseNiceVideoPlayer();
}
}
}
});

效果显示

到此为止,整个自定义视频播放器的流程我们已经基本掌握了。想要优化视频播放器可以去看JiaoziVideoPlayer等开源视频播放器的源码。

本篇基于NiceVideoPlayer源码,也可以去我的库中查看加了注释的简化版SimpleNiceVideoPlayer

0%