随着快手,抖音,西瓜视频等视频APP的崛起,视频播放已经成为主流,此时做为Android研发的你,想要提升本身的能力还不知道怎么开发视频播放器怎么行?
视频播放器有原生的VideoView、开源的Ijkplayer 、ExoPlayer、JieCaoVideoPlayer等等,这些部分的播放器我们在开发过程中,使用过这些视频播放框架来播放本地视频或者网络视频,但不一定会满足业务需求的。因此,我们可以去自定义一个播放器,在开发Android应用的过程中,难免需要自定义View,其实自定义View不难,只要了解原理,实现起来就没有那么难,从而满足业务需求。
先看效果图。
我自定义一个VideoPlayerView类,主要是由MediaPlayer与SurfaceView相结合组成一个视频播放器,从Android原生的VideoView视频框架,也是基于实现的。
2.编写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添加回调
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(callback);
//设置Surface不维护自己的缓冲区,而是等待屏幕的渲染引擎将内容推送到界面
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被创建");
if (currentPosition > 0) {
// 创建SurfaceHolder的时候,如果存在上次播放的位置,则按照上次播放位置进行播放
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被销毁");
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(), "视频文件路径错误", Toast.LENGTH_SHORT).show();
return;
}
LogUtil.info(TAG, "指定视频源路径");
try {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置播放的视频源
mMediaPlayer.setDataSource(file.getAbsolutePath());
// 设置显示视频的SurfaceHolder
mMediaPlayer.setDisplay(mSurfaceHolder);
LogUtil.info(TAG, "开始装载");
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
LogUtil.info(TAG, "装载完成");
mMediaPlayer.start();
// 按照初始位置播放
mMediaPlayer.seekTo(msec);
// 设置进度条的最大进度为视频流的最大播放时长
mSeekBar.setMax(mMediaPlayer.getDuration());
tvCurrentTime.setText(DateUtil.getDate(msec, DateUtil.PARAMETER));
tvTotalTime.setText(DateUtil.getDate(mMediaPlayer.getDuration(), DateUtil.PARAMETER));
// 开始线程,更新进度条的刻度
new Thread() {
@Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
// 如果正在播放,没0.5.毫秒更新一次进度条
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) {
// 在播放完毕被回调
mPlyer.setEnabled(true);
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 发生错误重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
// 播放远程视频
private void showRemoteVideo(final int msec) {
String videoUrl2 = mUrl;
Uri uri = Uri.parse(videoUrl2);
try {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// 设置播放的视频源
mMediaPlayer.setDataSource(AppContextUtil.getContext(), uri);
// 设置显示视频的SurfaceHolder
mMediaPlayer.setDisplay(mSurfaceHolder);
LogUtil.info(TAG, "开始装载");
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
LogUtil.info(TAG, "装载完成");
mMediaPlayer.start();
// 按照初始位置播放
mMediaPlayer.seekTo(msec);
// 设置进度条的最大进度为视频流的最大播放时长
mSeekBar.setMax(mMediaPlayer.getDuration());
tvCurrentTime.setText(DateUtil.getDate(msec, DateUtil.PARAMETER));
tvTotalTime.setText(DateUtil.getDate(mMediaPlayer.getDuration(), DateUtil.PARAMETER));
// 开始线程,更新进度条的刻度
new Thread() {
@Override
public void run() {
try {
isPlaying = true;
while (isPlaying) {
// 如果正在播放,没0.5.毫秒更新一次进度条
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;
// 在播放完毕被回调
mPlyer.setEnabled(true);
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
// 发生错误重新播放
play(0);
isPlaying = false;
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private SeekBar.OnSeekBarChangeListener change = new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// 当进度条停止修改的时候触发
// 取得当前进度条的刻度
int progress = seekBar.getProgress();
// if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
// 设置当前播放的位置
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");
}
}
该类主要做了如下工作:
3.编写Activity的布局文件activity_player_video.xml
从上面可以看出,里面没有任何子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() {
//这里传入本地视频的路径或者网络视频的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;
}
}
}
上面实例化一个VideoPlayerView,然后通过VideoPlayerView对象来调用setPlayerVideo的方法传入本地视频的路径或者网络视频的url,实现音视频播放器,运行此项目,效果如开头显示的一幕。
自定义View播放器已经实现了,但我们需要掌握MediaPlayer的初始化过程是怎么样的,MediaPlayer做了哪些工作,音视频数据如何渲染到SurfaceView上面的,这需要把MediaPlayer框架原理搞清楚。就像下面的。
每个方法调用过程是怎么样子的,做了哪些工作,而不是简单写几行代码就可以实现播放视频或者音频。
小伙伴们有兴趣的话,搜索并关注公众号“Android技术迷”关注后可阅读更多文章,感谢各位关注。