企业级app组件化开发4-----视频播放组件开发1

在大型的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绑定,开始加载视频数据,在视频播放的完成或者失败的情况下,会走onCompletiononError方法;

在此类中,主要是做了事件的触发回调,主要的事件处理,在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可以添加动画等等功能,在后续的章节中,将会继续完善该播放器的功能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Awesome_lay

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值