一、引言

在经过将近半个月的调研开发,我终于算是对“Android短视频开发”入门了。为了给自己一个总结,也为了把自己潜心研究的成果分享出来,我决定写下这篇blog。

播放器已经封装好,结构很清晰,直接调用即可。这里附上效果图和demo(点击下载)!

 

其中,图一为全屏播放,图二为播放中,图三为暂停,图四为上滑展示小窗口,图五为网络异常提示

    

 

二、方案选择

1、方案一:videoView+mediaPlayer

videoView继承自SurfaceView。surfaceView是在现有View上创建一个新的Window,内容显示和渲染是在新的Window中,这使得SurfaceView的绘制和刷新可以在单独的线程中进行。由于SurfaceView的内容是在新建的Window中,这使得SurfaceView不能放在RecyclerView或ScrollView中,一些View中的特性也无法使用。

 

2、方案二:textureView+mediaPlayer

textureView不会创建新的窗口,它的使用跟其他普通View一样。

 

考虑到以后的可扩展性,最终采用方案二。

 

三、TextureView介绍

1、TextureView被创建后不能直接使用,必须将其添加到ViewGroup中

2、TextureView必须要等SurfaceTexture准备就绪才能起作用,这里通常需要给TextureView设置监听器SurfaceTextureListener。等待onSurfaceTextureAvailable回调后,才能使用。

    @Override    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {        //SurfaceTexture准备就绪        if (mSurfaceTexture == null) {            mSurfaceTexture = surfaceTexture;            openMediaPlayer();        } else {            mTextureView.setSurfaceTexture(mSurfaceTexture);        }    }    @Override    public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {        //SurfaceTexture缓冲大小变化    }    @Override    public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {        //SurfaceTexture即将被销毁        return mSurfaceTexture == null;    }    @Override    public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {        //SurfaceTexture通过updateImage更新    }

SurfaceTexture的各个变化都有相应的回调方法。其中最重要的是onSurfaceTextureAvailable方法,该方法在surfaceTexture创建好后回调。主要在该方法中关联MediaPlayer,作为视频数据的图像来源

 

四、MediaPlayer介绍

1、几种重要的状态

idle:空闲状态。当mediaPlayer没有prepareAsync之前,就是处于idle状态。

prepared:准备好状态。想要让mediaPlayer开始播放,不能直接start,必须要先prepareSync。这期间mediaPlayer会一直在准备preparing,直到进入prepared状态。

started:当mediaPlayer准备好,就可以调用mediaPlayer的start方法进入started状态。

paused:当调用pause方法,进入paused状态。

completed:播放完成,进入completed状态。

error:播放错误。

 

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;    }

 

五、封装自己的短视频播放器

以上是短视频开发的知识点,如果是初识视频开发,可能会不清楚讲的是什么,可以接着看下面的视频播放器的封装。这里会详细地介绍如何利用TextureView和MediaPlayer封装一个属于自己的视频播放器。由于MediaPlayer的方法和状态比较多,播放暂停操作在不同状态下应调用不同方法。

一个短视频播放控件应该包含两层:顶层是播放器的控制器mController,底层是播放视频内容的TextureView。这里将这两层封装在一个容器FrameLayout中。

    public TourVideoPlayer(@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);    }

 

    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);    }
    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());        // TODO: 2018/1/4 待确定        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;          //小窗口模式

全屏、小窗口播放的实现

如何实现全屏?

将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);    }

 

六、最后

由于日常工作繁忙,这里博客写的可能没有很详细,不过demo写的非常详细,应该会是你想要的~~~~

待日后有时间时,我会进一步优化一下。

有任何问题,欢迎留言或邮件联系zhshan@ctrip.com

最后再次附上demo。



 

 

 

 

 

更多相关文章

  1. android vold初始化及sd卡挂载流程
  2. Android(安卓)转屏那些事儿
  3. 关于Android(安卓)7.0系统通知声音不能播放
  4. Android获取View的宽度和高度
  5. Android(安卓)6.0以上权限拒绝打开权限设置界面的解决方法
  6. 大厂面试,居然还问这些问题!
  7. Android(安卓)字体国际化适配方法以及源码解析
  8. Android(安卓)Binder 应用层调用过程分析
  9. IOS各项生命周期

随机推荐

  1. android Camera摄像头竖屏显示
  2. Android图片的固定大小显示
  3. Android(安卓)Studio cannot launch avd
  4. android有效解决加载大图片内存溢出的问
  5. Android媒体的一些使用总结
  6. EditText 设置 imeOptions 无效问题
  7. android面试题总结
  8. Android使用SQlite数据库
  9. 一分钟让你实现Android微信分享功能
  10. Android面试系列文章2018之Android部分In