V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wheat7
V2EX  ›  Android

从零开始打造一个 VR 播放器-Android

  •  
  •   wheat7 · 2017-08-01 21:58:00 +08:00 · 11385 次点击
    这是一个创建于 2674 天前的主题,其中的信息可能已经有所发展或是发生改变。

    项目地址

    我的博客

    简介

    VRPlayer 是一个本地 VR 视频播放器,整体使用了 DataBinding,MVVM 架构,播放部分基于 IJKPlayer,VR 渲染部分基于 MD360Player4Android,UI 上部分使用了 Carbon,沉浸式状态栏使用了 StatusBarUtil 这个项目,图片加载使用 Glide
    VRPlayer 会扫描你手机中的视频文件,然后你可以找到你要播放的 VR 视频文件,点击即可播放

    效果


    分析

    项目主要分三部分,一是重写的 MediaController,二就是播放器的包装类,也相当于我们的原生的 VideoView,源码中为 PlayerView,最后一部分将 PlayerView 和 MD360Player 库中的 VRLibrary 整合包装,也就是源码中的 VRPlayerView,实现 VR 模式控制的接口,在使用的时候就只需要添加这一个 View 本文主要分析拓展的部分,因为并不是 VideoView,MediaController,或是 IJk Demo 的分析,所以重合部分就不作分析了,如果同学们对这一部分不熟悉,可以自行学习, PlayerView 的包装可以参考原生 VideoView 以及 IJkPlayer 的 Demo, MediaController 可参考原生代码, VRPlayerView 可以参考 MD360Player 的 Demo

    播放部分根据 IJkPlayer 的 Demo 进行修改,重写 MediaController,IJKPlayer 的 Demo 也是根据原生的 VideoView 进行修改,但并没有自定义 MediaController,原生的 VideoView+MediaController 想必做过视频播放的同学都比较熟悉了

    mVideoView.setMediaController(mMediaController);
    mMediaController.setMediaPlayer(mVideoView);
    

    优点在于播放和控制解耦,但是原生的 MediaController 类可定制性很低,创建 View 的方法都是私有,并且用到了 PhoneWindow 这种系统内部才开放的类
    创建 Window 时使用了 PhoneWindow

    mWindow = new PhoneWindow(mContext);
    

    初始化 Controller 的方法私有

    private void initControllerView(View v) {
    }
    
    

    所以通过继承来自定义是不可能的,我看到的好多开源项目都是将播放以及控制写到一个 View 中,但是我并不认为这是一种优雅的方式,所以我们得自己来重写 MediaController

    重写 MediaController

    先把 MediaController 复制粘贴一份,所以重合部分请参考原生 MediaController 以及源码中的 VRMediaController
    下边根据几个关键点给大家讲解分析

    将 Window 改为 PopWindow

    • 定义
    private PopupWindow mWindow;
    
    • 初始化
    private void initFloatingWindow() {
        mWindow = new PopupWindow(mContext);
        mWindow.setFocusable(false);
        mWindow.setBackgroundDrawable(null);
        mWindow.setOutsideTouchable(true);
        mAnimStyle = android.R.style.Animation;
        requestFocus();
        }
    
    • 然后在 setAnchorView(View view)方法中,把 Controller 的 View 放到 PopWindow 中,makeControllerView()返回的是 Controller 的 View,下边会进行讲解
        public void setAnchorView(View view) {
            mAnchor = view;
            if (!mFromXml) {
                removeAllViews();
                mRoot = makeControllerView();
                mWindow.setContentView(mRoot);
                mWindow.setWidth(LayoutParams.MATCH_PARENT);
                mWindow.setHeight(LayoutParams.WRAP_CONTENT);
            }
            initControllerView(mRoot);
        }
    

    下边讲解 Controller 的创建

    Controller 的创建

    • 在 makeControllerView()方法中生成 Controller 的 View
        protected View makeControllerView() {
            return ((LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(getResources().getIdentifier("media_controller_vr", "layout", mContext.getPackageName()), this);
        }
    

    这里我们取得 View 的方式参考原生,使用 getSystemService 的方式获取,如果同学觉得不好,可以使用常规方式获取,原生采用这样的方式获取,我猜是为了防止包名变化,获取不到 View

    • View 在源码中对应 media_controller_vr.xml

    篇幅有限,不贴了

    最后两个 ImageView 是我们 VR 控制的部分,后边要编写对应的接口

    • 初始化 Controller 初始化 Controller 和原生的并并无二致,重点是要设置相应的监听
    private void initControllerView(View v) {
            mPauseButton = (ImageView) v.findViewById(getResources().getIdentifier("mediacontroller_play_pause", "id", mContext.getPackageName()));
            if (mPauseButton != null) {
                mPauseButton.requestFocus();
                mPauseButton.setOnClickListener(mPauseListener);
            }
    
            mVRInteractiveModeButton = (ImageView) v.findViewById(getResources().getIdentifier("mediacontroller_interactive", "id", mContext.getPackageName()));
            if (mVRInteractiveModeButton != null) {
                mVRInteractiveModeButton.requestFocus();
                mVRInteractiveModeButton.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mVRControl.onInteractiveClick(interactiveMode);
                        updateInteractive();
                    }
                });
            }
            mVRDisplayModeButton = (ImageView) v.findViewById(getResources().getIdentifier("mediacontroller_display", "id", mContext.getPackageName()));
            if (mVRDisplayModeButton != null) {
                mVRDisplayModeButton.requestFocus();
                mVRDisplayModeButton.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mVRControl.onDisplayClick(displayMode);
                        updateDisplay();
                    }
                });
            }
    
            mProgress = (SeekBar) v.findViewById(getResources().getIdentifier("mediacontroller_seekbar", "id", mContext.getPackageName()));
            if (mProgress != null) {
                if (mProgress instanceof SeekBar) {
                    SeekBar seeker = (SeekBar) mProgress;
                    seeker.setOnSeekBarChangeListener(mSeekListener);
                }
                mProgress.setMax(1000);
            }
    
            mEndTime = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_time_total", "id", mContext.getPackageName()));
            mCurrentTime = (TextView) v.findViewById(getResources().getIdentifier("mediacontroller_time_current", "id", mContext.getPackageName()));
    
            mFormatBuilder = new StringBuilder();
            mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
        }
    

    这样,Controller 的 View 的创建过程就完成了

    Progress、show、hide

    • 自定义 Handler 更新 Progress,show、hide View
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                long pos;
                switch (msg.what) {
                    case FADE_OUT:
                        hide();
                        break;
                    case SHOW_PROGRESS:
                        pos = setProgress();
                        if (!mDragging && mShowing) {
                            msg = obtainMessage(SHOW_PROGRESS);
                            sendMessageDelayed(msg, 1000 - (pos % 1000));
                            updatePausePlay();
                        }
                        break;
                }
            }
        };
    

    因为刷新 View 的方式改为了自定义的 Handler,所以相应部分的代码要进行修改,具体情参见源码
    并且因为 Window 改为了 PopupWindow,在 show、hide 时候,要将

    mWindowManager.addView(mDecor, mDecorLayoutParams);
    mWindowManager.removeView(mDecor);
    

    改为直接操作 View,即

    setVisibility(View.VISIBLE);
    setVisibility(View.GONE);
    

    扩展

    VRMediaController 主要拓展了 VR 播放模式的控制,以及在视频上方添加一个可定制的 Title 的功能

    • VR 播放模式控制 播放模式主要是结合 MD360Player 库使用,提供回调接口,这里我使用了 interactiveMode 的 INTERACTIVE_MODE_CARDBORAD_MOTION、INTERACTIVE_MODE_TOUCH,主要是切换播放时的画面控制,陀螺仪控制以及触摸控制模式; DisplayMode 的 MDVRLibrary.DISPLAY_MODE_GLASS 模式以及 MDVRLibrary.DISPLAY_MODE_NORMAL 模式,GLASS 模式播放双目的视频,我们可以将手机放到 VR 盒子里进行观看,NORMAL 播放单目视频
      我们已经在 Controller 布局文件里定义了两个按钮来切换这两种模式,现在在 Controller 中编写接口
        public interface VRControl {
            void onInteractiveClick(int currentMode);
            void onDisplayClick(int currentMode);
        }
    

    并且在点击时,切换按钮的图标

        private void updateInteractive() {
            if (mRoot == null || mVRInteractiveModeButton == null)
                return;
            if (interactiveMode == VR_INTERACTIVE_MODE_GYROSCOPE) {
                interactiveMode = VR_INTERACTIVE_MODE_TOUCH;
                mVRInteractiveModeButton.setImageResource(getResources().getIdentifier("ic_gyroscope", "drawable", mContext.getPackageName()));
            }
            else {
                interactiveMode = VR_INTERACTIVE_MODE_GYROSCOPE;
                mVRInteractiveModeButton.setImageResource(getResources().getIdentifier("ic_touch_mode", "drawable", mContext.getPackageName()));
            }
        }
    
        private void updateDisplay() {
            if (mRoot == null || mVRDisplayModeButton == null)
                return;
            if (displayMode == VR_DISPLAY_MODE_GLASS) {
                displayMode = VR_DISPLAY_MODE_NORMAL;
                mVRDisplayModeButton.setImageResource(getResources().getIdentifier("ic_vr_mode", "drawable", mContext.getPackageName()));
            }
            else {
                displayMode = VR_DISPLAY_MODE_GLASS;
                mVRDisplayModeButton.setImageResource(getResources().getIdentifier("ic_eye_mode", "drawable", mContext.getPackageName()));
            }
        }
    
    • Title
      Title 主要是实现在视频的上方添加一个自定义的 Title,实现返回等功能的实现,并且和 Controller 一起 show、hide, TitleView 就是一个普通的 View,在使用 VRPlayerView 时候定义,代码在使用介绍中有提到

    然后通过 setTitleView(View v)方法传入 Controller,并在 show()、hide()方法中和 Controller 一同 show、hide 就可以了

    PlayerView 包装

    PlayerView 就类似于原生的 VideoView,准确的说,就是从 ViedoView 修改过来的,其就是一个 MediaPlayer 的包装类,与 Mediaplayer 结合,实现各种控制的回调,不了解的同学可以参考 VideoView 源码和 IJKPlayer 的 Demo,不同的是,VideoView 直接继承了 SurfaceView 作为播放显示的 View,我们这里做了修改,继承了 FrameLayout,因为播放 VR 视频使用的是 MD360Player 的 OpenGl 库中提供的 GLSurfaceView,并通过 Media 的 setSurfacefan 方法进行设置,但是在 PlayerVie 中还是以 addView 的方式添加 SurfaceView,可以通过 setSurfaceView(SurfaceView surfaceView)方法传入,以便扩展,下边主要讲解扩展部分

    • 扩展 扩展部分主要是添加了 IJKPlayer 的硬解码功能,这也是 MD360Player 的 Demo 中添加的,如果加入硬解,播放会很卡顿,并且发热量很大
        private void enableHardwareDecoding(){
            if (mMediaPlayer instanceof IjkMediaPlayer){
                IjkMediaPlayer player = (IjkMediaPlayer) mMediaPlayer;
                player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 1);
                player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 1);
                player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "overlay-format", IjkMediaPlayer.SDL_FCC_RV32);
                player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 60);
                player.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-fps", 0);
                player.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
            }
        }
        
    

    然后在 mMediaPlayer 创建以后调用 enableHardwareDecoding()即可

    VRPlayerView-PlayerView 与 VRLibrary 整合

    VRPlayerView 主要是将 PlayerViewGLSurfaceView 进行包装,并与 MDVRLibraryj 进行整合,实现 VRMediaController.VRControl 接口,控制 VR 播放模式,在使用时,在布局中添加一个 VRPlayerView 即可

    • VRPlayerView 继承于 FrameLayout,初始化时候,依次 add GLSurfaceView、PlayerView,并且设置 MediaController
     private void init() {
            setKeepScreenOn(true);
            mGLSurfaceView=new GLSurfaceView(getContext());
            addView(mGLSurfaceView);
            mPlayerView = new PlayerView(getContext());
            addView(mPlayerView);
            mMediaController = new VRMediaController(getContext());
            mPlayerView.setMediaController(mMediaController);
            mMediaController.setMediaPlayer(mPlayerView);
            mMediaController.setOnVRControlListener(this);
            initVRLibrary();
        }
    
    • 初始化 VRLibrary
      MD360Player 有很多使用方法,是个强大的库,具体使用可以参见该项目,下边初始化我们用到的
    private void initVRLibrary() {
            // new instance
            mVRLibrary = MDVRLibrary.with(getContext())
                    .displayMode(MDVRLibrary.DISPLAY_MODE_GLASS)
                    .interactiveMode(MDVRLibrary.INTERACTIVE_MODE_CARDBORAD_MOTION)
                    .projectionMode(MDVRLibrary.PROJECTION_MODE_SPHERE)
                    .pinchConfig(new MDPinchConfig().setDefaultValue(0.7f).setMin(0.5f))
                    .pinchEnabled(true)
                    .directorFactory(new MD360DirectorFactory() {
                        @Override
                        public MD360Director createDirector(int index) {
                            return MD360Director.builder().setPitch(90).build();
                        }
                    })
                    .asVideo(new MDVRLibrary.IOnSurfaceReadyCallback() {
                        @Override
                        public void onSurfaceReady(Surface surface) {
                            // IjkMediaPlayer or MediaPlayer
                            mPlayerView.getPlayer().setSurface(surface);
                        }
                    })
                    .build(mGLSurfaceView);
            mVRLibrary.setAntiDistortionEnabled(true);
        }
    
    
    • 实现 VRMediaController.VRControl 接口
        @Override
        public void onInteractiveClick(int currentMode) {
            if (currentMode == MDVRLibrary.INTERACTIVE_MODE_CARDBORAD_MOTION) {
                mVRLibrary.switchInteractiveMode(getContext(), MDVRLibrary.INTERACTIVE_MODE_TOUCH);
            } else {
                mVRLibrary.switchInteractiveMode(getContext(), MDVRLibrary.INTERACTIVE_MODE_CARDBORAD_MOTION);
            }
        }
    
        @Override
        public void onDisplayClick(int currentMode) {
            if (currentMode == MDVRLibrary.DISPLAY_MODE_GLASS) {
                mVRLibrary.switchDisplayMode(getContext(), MDVRLibrary.DISPLAY_MODE_NORMAL);
                mVRLibrary.setAntiDistortionEnabled(false);
            } else {
                mVRLibrary.switchDisplayMode(getContext(), MDVRLibrary.DISPLAY_MODE_GLASS);
                mVRLibrary.setAntiDistortionEnabled(true);
            }
        }
    
    • 包装一些方法方便调用
      public AbstractMediaPlayer getMediaPlayer() {
            return mPlayerView.getPlayer();
        }
    
        public PlayerView getPlayerView() {
            return mPlayerView;
        }
    
        public void setVideoPath(String path) {
            mPlayerView.setVideoPath(path);
        }
    
        public void setVideoUri(Uri uri) {
            mPlayerView.setVideoURI(uri);
        }
    
        public void setMediaControllerTitle(View v) {
            mMediaController.setTitleView(v);
        }
    
    • 生命周期控制 生命周期控制是必须的,具体可以参考 MD360Player
       public void onPause(){
            if (mVRLibrary!=null)mVRLibrary.onPause(getContext());
            if (mPlayerView!=null)mPlayerView.pause();
        }
        public void onResume(){
            if (mVRLibrary!=null)
                mVRLibrary.onResume(getContext());
            if (mPlayerView!=null)
                mPlayerView.resume();
        }
        public void onDestroy(){
            if (mVRLibrary!=null) mVRLibrary.onDestroy();
            if (mPlayerView!=null) mPlayerView.stopPlayback();
        }
    

    使用

    • 在使用的时候,在布局文件中添加 VRPlayerView 以及 TitleView
     <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
        <com.wheat7.vrplayer.vr.VRPlayerView
            android:id="@+id/player"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    
            <RelativeLayout
                android:id="@+id/mediacontroller_title"
                android:layout_width="match_parent"
                android:layout_height="33dp"
                android:background="@color/mediacontroller_bg"
                android:visibility="gone">
    
                <carbon.widget.ImageView
                    android:layout_marginTop="3dp"
                    android:layout_width="25dp"
                    android:layout_height="25dp"
                    android:clickable="true"
                    android:onClick="@{()-> activity.onBackClick()}"
                    android:src="@drawable/ic_back" />
            </RelativeLayout>
    
        </FrameLayout>
    
    • 初始化 Databinding 方式
    getBinding().player.setVideoPath(urlStr);
            getBinding().player.setMediaControllerTitle(getBinding().mediacontrollerTitle);
    
    • 生命周期控制
        @Override
        protected void onPause() {
            super.onPause();
            getBinding().player.onPause();
    
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            getBinding().player.onDestroy();
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            getBinding().player.onResume();
        }
    

    题外话

    Databinding BaseActivity 封装

    对于 Databinding 的使用,相信同学们已经非常熟悉了,现在分享一种 Databinding 的 BaseActivity 的封装方式

    public abstract class BaseActivity<T extends ViewDataBinding> extends AppCompatActivity {
    
        private View mainView;
        private ViewDataBinding binding;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            int layoutId = getLayoutId();
            super.onCreate(savedInstanceState);
            try {
                binding = DataBindingUtil.setContentView(this, layoutId);
                if (binding != null) {
                    mainView = binding.getRoot();
                } else {
                    mainView = LayoutInflater.from(this).inflate(layoutId, null);
                    setContentView(mainView);
                }
    
            } catch (NoClassDefFoundError e) {
                mainView = LayoutInflater.from(this).inflate(layoutId, null);
                setContentView(mainView);
            }
            initView(savedInstanceState);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
        }
    
    
        public T getBinding() {
            return (T) binding;
        }
    
        public abstract int getLayoutId();
    
        public abstract void initView(Bundle savedInstanceState);
    
    }
    
    

    通过泛型参数将相应 Binding 类传入,然后就可以通过 getBinding()方法获取对应的 Binding 类,通过 getLayoutId()方法传入布局,在使用时在 initView()中初始化 Activity

    其他

    项目还包括一些其他的东西,包括 Databinding 的 ViewHolder、欢迎界面的闪动 TextView、沉浸式状态栏工具类 StatusBarUtil 的使用等,就不作赘述了,详见源码,如果有要和我讨论的同学,可以联系我哦

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5471 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 07:42 · PVG 15:42 · LAX 23:42 · JFK 02:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.