1、前言
上一篇文章我们介绍了SurfaceView和TextureView的基础知识点;
SurfaceView 以及 TextureView 均继承于 android.view.View,属于 Android 提供的控件体系的一部分。与普通 View 不同,它们都在独立的线程中绘制和渲染。所以,相比于普通的 ImageView 它们的性能更高,因此常被用在对绘制的速率要求比较高的应用场景中,用来解决普通 View 因为绘制的时间延迟而带来的掉帧的问题,比如用作相机预览、视频播放的媒介等;
今天我们就来简单的用TextureView封装下视频播放器;
2、视频播放器方案介绍
1.videoView+mediaPlayer
videoView继承自SurfaceView。surfaceView是在现有View上创建一个新的Window,
内容显示和渲染是在新的Window中,这使得SurfaceView的绘制和刷新可以在单独的线程中进行。
由于SurfaceView的内容是在新建的Window中,这使得SurfaceView不能放在RecyclerView或ScrollView中,一些View中的特性也无法使用。
2.textureView+mediaPlayer
textureView不会创建新的窗口,它的使用跟其他普通View一样。
考虑到以后的可扩展性,最终采用这个方案
3.为什么使用TextureView
TextureView是在4.0(API level 14)引入的,与SurfaceView相比,它不会创建新的窗口来显示内容。它是将内容流直接投放到View中,并且可以和其它普通View一样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中使用。
3、TextureView使用介绍
1.TextureView被创建后不能直接使用,必须将其添加到ViewGroup中。
2.TextureView必须要等SurfaceTexture准备就绪才能起作用,这里通常需要给TextureView设置监听器SurfaceTextureListener。等待onSurfaceTextureAvailable回调后,才能使用
3.TextureView创建和初始化
//初始化一个TextureView并添加至ViewGroup或找到你的TextureView 组件
mTextureView=new TextureView(getContext());
//设置画布监听
textureView.setSurfaceTextureListener(this);
//添加至布局
fragment.addView(textureView,new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT, Gravity.CENTER));
/**
* TextureView准备好了回调
* @param surface 内部画布渲染surface
* @param width TextureView布局宽
* @param height TextureView布局高
*/
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Logger.d(TAG,"onSurfaceTextureAvailable-->width:"+width+",height:"+height);
//这里对画面改变、转场播放做了处理,声明一个mSurfaceTexture ,在TextureView发生变化时更新
if (mSurfaceTexture == null) {
mSurfaceTexture = surface;
//prepare();
} else {
mTextureView.setSurfaceTexture(mSurfaceTexture);
}
}
/**
* TextureView宽高发生变化时回调
* @param surface 内部surface
* @param width 新的TextureView布局宽
* @param height 新的TextureView布局高
*/
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
Logger.d(TAG,"onSurfaceTextureSizeChanged-->width:"+width+",height:"+height);
}
/**
* TextureView销毁时回调
* @param surface 内部surface
* @return Most applications should return true.
*/
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Logger.d(TAG,"onSurfaceTextureDestroyed");
return null==mSurfaceTexture;
}
/**
* TextureView刷新时回调
* @param surface 内部surface
*/
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
4、MediaPlayer介绍
1.重要的状态
-
idle:空闲状态。当mediaPlayer没有prepareAsync之前,就是处于idle状态。
-
prepared:准备好状态。想要让mediaPlayer开始播放,不能直接start,必须要先prepareSync。这期间mediaPlayer会一直在准备preparing,直到进入prepared状态。
-
started:当mediaPlayer准备好,就可以调用mediaPlayer的start方法进入started状态。
-
paused:当调用pause方法,进入paused状态。
-
completed:播放完成,进入completed状态。
-
error:播放错误。
【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~
2.重要的方法
-
prepareAsync:要想使用mediaPlayer,必须先调用prepareAsync。这是第一步。
-
start:开始
-
pause:暂停
-
reset:播放完成后,如想重新开始,调用该方法。
3.重要的回调
-
onSurfaceTextureAvailable:开始关联mediaPlayer
-
onPrepared:此处开始调用mediaPlayer.start()
-
onInfo:播放开始后,视频到底状态如何,就是在onInfo中处理
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
// 播放器渲染第一帧
mCurrentState = STATE_PLAYING;
mController.onPlayStateChanged(mCurrentState);
} else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
// MediaPlayer暂时不播放,以缓冲更多的数据
if (mCurrentState == STATE_PAUSED || mCurrentState == STATE_BUFFERING_PAUSED) {
mCurrentState = STATE_BUFFERING_PAUSED;
} else {
mCurrentState = STATE_BUFFERING_PLAYING;
}
mController.onPlayStateChanged(mCurrentState);
} else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
// 填充缓冲区后,MediaPlayer恢复播放/暂停
if (mCurrentState == STATE_BUFFERING_PLAYING) {
mCurrentState = STATE_PLAYING;
mController.onPlayStateChanged(mCurrentState);
}
if (mCurrentState == STATE_BUFFERING_PAUSED) {
mCurrentState = STATE_PAUSED;
mController.onPlayStateChanged(mCurrentState);
}
} else {
LogUtil.d("onInfo ——> what:" + what);
}
return true;
}
4.MediaPlayer初始化和准备播放
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
//设置准备播放监听器,在onPrepared回调中开始播放
mMediaPlayer.setOnPreparedListener(this);
//...此处省去一系列监听设置
//异步准备
mMediaPlayer.prepareAsync();
/**
* 播放器准备好了
* @param mp 解码器
*/
@Override
public void onPrepared(MediaPlayer mp) {
Logger.d(TAG,"onPrepared");
if(null!=mSurfaceTexture){
if(null!=mSurface){
mSurface.release();
mSurface=null;
}
mSurface =new Surface(mSurfaceTexture);
mp.setSurface(mSurface);
}
//开始播放
mp.start();
}
5、封装视频播放器
5.1 封装播放器
视频播放控件应该包含两层:顶层是播放器的控制器mController,底层是播放视频内容的TextureView。这里将这两层封装在一个容器FrameLayout中;
public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
if (mNetworkChangeReceiver == null) {
mNetworkChangeReceiver = new NetworkChangeReceiver(this);
}
allow4GFlag = false;
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);
}
addTextureView
private void addTextureView() {
mContainer.removeView(mTextureView);
LayoutParams params = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.CENTER);
mContainer.addView(mTextureView, 0, params);
}
setController
public void setController(IVideoController controller) {
mContainer.removeView(mController);
mController = controller;
mController.reset();
mController.setVideoPlayer(this);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mContainer.addView(mController, params);
}
播放,将TextureView、MediaPlayer、Controller进行初始化。待TextureView的数据通道SurfaceTexture准备就绪后,打开播放器
private void openMediaPlayer() {
// 屏幕常亮
mContainer.setKeepScreenOn(true);
// 设置监听
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnVideoSizeChangedListener(this);
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setOnErrorListener(this);
mMediaPlayer.setOnInfoListener(this);
mMediaPlayer.setOnBufferingUpdateListener(this);
mCurrentNetworkState = NetworkChangeReceiver.getNetworkStatus(CtripBaseApplication.getInstance());
mNetworkChangeReceiver.registerNetworkChangeBroadcast();
// 设置dataSource
try {
mMediaPlayer.setDataSource(mUrl);
if (mSurface == null) {
mSurface = new Surface(mSurfaceTexture);
}
mMediaPlayer.setSurface(mSurface);
mMediaPlayer.prepareAsync();
mCurrentState = STATE_PREPARING;
mController.onPlayStateChanged(mCurrentState);
} catch (IOException e) {
e.printStackTrace();
LogUtil.e("打开播放器发生错误", e);
}
}
private void initMediaPlayer() {
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
}
private void initTextureView() {
if (mTextureView == null) {
mTextureView = new TourTextureView(mContext);
mTextureView.setSurfaceTextureListener(this);//此时回调onSurfaceTextureAvailable
}
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
if (mSurfaceTexture == null) {
mSurfaceTexture = surfaceTexture;
openMediaPlayer();
} else {
mTextureView.setSurfaceTexture(mSurfaceTexture);
}
}
播放逻辑写完之后,具体UI展示逻辑在VideoPlayerController中。根据不同的状态VideoPlayerController展示不同UI
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; //播放完成
public static final int STATE_NOTE_4G = 8; //提示4G
public static final int STATE_NOTE_DISCONNECT = 9; //提示断网
public static final int MODE_NORMAL = 10; //普通模式
public static final int MODE_FULL_SCREEN = 11; //全屏模式
public static final int MODE_TINY_WINDOW = 13; //小窗口模式
5.2 全屏、小窗口播放的实现
实现全屏:将mContainer移除,并添加到android.R.content中,并设置成横屏
@Override
public void enterFullScreen() {
if (mCurrentMode == MODE_FULL_SCREEN) return;
// 隐藏ActionBar、状态栏,并横屏
TourVideoUtil.hideActionBar(mContext);
TourVideoUtil.scanForActivity(mContext)
.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
new Handler().post(new Runnable() {
@Override
public void run() {
ViewGroup contentView = (ViewGroup) TourVideoUtil.scanForActivity(mContext)
.findViewById(android.R.id.content);
if (mCurrentMode == MODE_TINY_WINDOW) {
contentView.removeView(mContainer);
} else {
TourVideoPlayer.this.removeView(mContainer);
}
LayoutParams params = new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
contentView.addView(mContainer, params);
}
});
mCurrentMode = MODE_FULL_SCREEN;
mController.onPlayModeChanged(mCurrentMode);
}
实现小窗口:将mContainer移除,添加到android.R.content中,并设置宽高
@Override
public void enterTinyWindow() {
if (mCurrentMode == MODE_TINY_WINDOW) return;
this.removeView(mContainer);
new Handler().post(new Runnable() {
@Override
public void run() {
ViewGroup contentView = (ViewGroup) TourVideoUtil.scanForActivity(mContext)
.findViewById(android.R.id.content);
// 小窗口的宽度为屏幕宽度的60%,长宽比默认为16:9,右边距、下边距为8dp。
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
(int) (CommonUtil.getScreenWidth(mContext) * 0.6f),
(int) (CommonUtil.getScreenWidth(mContext) * 0.6f * 9f / 16f));
params.gravity = Gravity.TOP | Gravity.START;
params.topMargin = CommonUtil.dp2px(mContext, 48f);
contentView.addView(mContainer, params);
}
});
mCurrentMode = MODE_TINY_WINDOW;
mController.onPlayModeChanged(mCurrentMode);
}
6、总结
关于视频播放器封装的知识点还有很多,今天知识简单的介绍了下封装的步骤和思路;
大家如果想自己封装可以参考网上NiceVieoPlayer;
以后会继续讲解关于视频播放器的知识点;