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

2017-08-01 21:58:00 +08:00
 wheat7

项目地址

我的博客

简介

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();
    }
    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 的创建

    protected View makeControllerView() {
        return ((LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(getResources().getIdentifier("media_controller_vr", "layout", mContext.getPackageName()), this);
    }

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

篇幅有限,不贴了

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

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

    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 的功能

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

然后通过 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)方法传入,以便扩展,下边主要讲解扩展部分

    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 即可

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

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

使用

 <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>
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 的使用等,就不作赘述了,详见源码,如果有要和我讨论的同学,可以联系我哦

7724 次点击
所在节点    Android
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/379650

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX