zoukankan      html  css  js  c++  java
  • 带着问题写React Native原生控件--Android视频直播控件

    最近在做的采用React Native项目有一个需求,视频直播与直播流播放同一个布局中,带着问题去思考如何实现,能更容易找到问题关键点,下面分析这个控件解决方法:

    现在条件:视频播放控件(开源的ijkplayer),直播控件(自定义控件继承自TextureView与SurfaceView)

    1.两种控件切换方式?

    讲到切换方式,那应该是从一个布局切换到另一个布局,那如何进行布局,可以是两种布局:嵌套布局(直播控件包括播放控件),单独布局(先移除容器的控件后添加所需控件),采用第二种方式进行实现。

    2.如何实现原生控件?

    demo的基本功能包括推流,结束推流,播放直播流,前后摄像头切换。

    实现控件需要申明两个基本的类:RNLiveViewManager(直播布局管理类)与RNLiveView(直播布局类)

    一 RNLiveViewManager

    原生视图需要被一个ViewManager的派生类(或者更常见的,SimpleViewManage的派生类)创建和管理。一个SimpleViewManager可以用于这个场景,是因为它能够包含更多公共的属性,譬如背景颜色、透明度、Flexbox布局等等。

    提供原生视图很简单:

    1. 创建一个ViewManager的子类。
    2. 实现createViewInstance方法。
    3. 导出视图的属性设置器:使用@ReactProp(或@ReactPropGroup)注解。
    4. 把这个视图管理类注册到应用程序包的createViewManagers里。
    5. 实现JavaScript模块。

    RNLiveView继承自FrameLayout,因此,需要继承ViewGroupManager进行RNLiveView管理。

    RNLiveViewManager:其中RNLiveViewManager的功能是桥梁,复杂调用原生的方法,并提供React调用。

    继承自ViewGroupManager:需要重写两个方法getName与createViewInstance

    1. 创建ViewManager的子类

    在这个例子里我们创建一个视图管理类ReactImageManager,它继承自SimpleViewManager<ReactImageView>ReactImageView是这个视图管理类所管理的对象类型,这应当是一个自定义的原生视图。getName方法返回的名字会用于在JavaScript端引用这个原生视图类型。

    public class RNLiveViewManager extends ViewGroupManager<RNLiveView> {
        public static final String REACT_CLASS = "RNLiveView";
    
        @Override
        public String getName() {
            return REACT_CLASS;
        }

    2. 实现方法createViewInstance

    视图在createViewInstance中创建,且应当把自己初始化为默认的状态。所有属性的设置都通过后续的updateView来进行。

      @Override
        public RNLiveView createViewInstance(ThemedReactContext context) {
            return new RNLiveView(context);
        }
    

    3. 通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。

    方法的第一个参数是要修改属性的视图实例,第二个参数是要设置的属性值。方法的返回值类型必须为void,而且访问控制必须被声明为public。 

        @ReactProp(name = "url")
        public void setUrl(RNLiveView view, @Nullable String url) {
            view.setUrl(url);//设置rtmp地址(推流地址或者直播流地址)
        }
    
        @ReactProp(name = "facing")
        public void setFacing(RNLiveView view, Integer pos) {
            view.setFacing(pos);//设置前后摄像头位置
        }
    
        @ReactProp(name = "mode")
        public void setMode(RNLiveView view, Integer mode) {
            view.setMode(mode);// 设置播放,直播,停止直播模式
        }
    

    4. 注册ViewManager

    在Java中的最后一步就是把视图控制器注册到应用中。这和原生模块的注册方法类似,唯一的区别是我们把它放到createViewManagers方法的返回值里。

       @Override
        public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
            return Arrays.<ViewManager>asList(
                    new RNIjkPlayerManager(),
                    new RNAvCaptureManager(),new RNLiveViewManager()
            );
        }
    

    5. 实现对应的JavaScript模块

    'use strict';
    import React, {Children} from 'React';
    var {View, Platform} = require('react-native');
    var PropTypes = React.PropTypes;
    const RNLiveViewManager = require('NativeModules').RNLiveViewManager;
    const is_ios = (Platform.OS === 'ios');
    
    import { requireNativeComponent } from 'react-native';
    const RCT_LIVEVIEW_REF = 'LiveView';
    
    var LiveView = React.createClass({
        propTypes: {
            ...View.propTypes,
            url: PropTypes.string,
            mode: PropTypes.number,
            facing: PropTypes.number,
        },
    
        componentDidMount: function() {
            this._mounted = true;
        },
    
        componentWillUnmount: function() {
            this._mounted = false;
        },
    
        onLiveViewEvent: function(event) {
            if (!this._mounted)
                return;
        },
    
        renderChildren: function() {
            return Children.map(this.props.children, (child) => child);
        },
    
        render: function() {
            return (<RNLiveView ref={RCT_LIVEVIEW_REF} style={this.props.style} onLiveViewEvent={this.onLiveViewEvent}
                    url={this.props.url} mode={this.props.mode} facing={this.props.facing}>
                    {this.renderChildren()}
                    </RNLiveView>);
        }
    });
    //设置导出的RNLiveView控件
    var RNLiveView = requireNativeComponent('RNLiveView', LiveView, {
      nativeOnly: {
        onLiveViewEvent: true,
      },
    });
    
    LiveView.FACING_BACK = 0;
    LiveView.FACING_FRONT = 1;
    LiveView.STOP = 0;
    LiveView.PUBLISH = 1;
    LiveView.PLAY = 2;
    
    module.exports = LiveView;
    

    注意上面用到了nativeOnly。有时候有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应React封装组件的属性。举个例子,Switch组件可能在原生组件上有一个onChange事件,然后在封装类中导出onValueChange回调属性。这个属性在调用的时候会带上Switch的状态作为参数之一。这样的话你可能不希望原生专用的属性出现在API之中,也就不希望把它放到propTypes里。可是如果你不放的话,又会出现一个报错。解决方案就是带上nativeOnly选项。  

    二 RNLiveView

    现在采用单独布局方式,根据mode值判断布局状态,移除已有的布局添加新的布局(即推流布局与直播流播放布局)。

    1. 基本思路实现

    讲下重写onLayout方法的作用:视频播放控件与直播控件是在最底层的,由于控制播放与直播的控件叠加在这之上,要处理如何摆放的问题?

    public class RNLiveView extends FrameLayout {
        private final int mScreenWidth;
        private final int mScreenHeight;
        private RNIjkPlayer rnIjkPlayer;
        private RNAvCapture rnAvCapture;
        private final Context mConntext;
        private String mUrl = "";
        private int mMode=0;
    
        public RNLiveView(@NonNull Context context) {
            super(context);
            this.mConntext = context;
        }
    
        public void setUrl(String url) {
            if (mUrl != null && mUrl.compareTo(url) == 0)
                return;
            this.mUrl = url;
        }
    
        public void setFacing(int pos) {
            if (rnAvCapture != null)
                rnAvCapture.setFacing(pos);
        }
    
        private RNAvCapture getRNAvCapture() {
            return new  RNAvCapture(mConntext);
        }
    
        //设置3种模式:停止,直播发布,视频播放
        public void setMode(int mode) {
            if (mMode != mode) {
                this.mMode=mode;
                 //停止
                 if (mode== 0) {
                            if (rnAvCapture != null) {
                                rnAvCapture.setStart(false);
                            }
                    }   //直播发布
                 else if (mode == 1) {
                            try {
                                if (rnIjkPlayer != null)
                                {
                                    RNLiveView.this.removeView(rnIjkPlayer);
                                }
                                rnAvCapture = getRNAvCapture();
                                rnAvCapture.setUrl(mUrl);
                                rnAvCapture.setStart(true);
                                RNLiveView.this.addView(rnAvCapture);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }  //视频播放
                   else if (mode == 2) {
                            try {
                                if (rnAvCapture != null) {
                                    rnAvCapture.setStart(false);
                                    RNLiveView.this.removeView(rnAvCapture);
                                }
                                rnIjkPlayer = RNIjkPlayer.getInstance(mConntext);
                                rnIjkPlayer.setUrl(mUrl);
                                rnIjkPlayer.setLive(false);
                                rnIjkPlayer.setFullScreen(false);
                                rnIjkPlayer.setIsMediaControl(false);
                                RNLiveView.this.addView(rnIjkPlayer);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
            }
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            this.removeAllViews();
        }
        
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            final int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (child != rnIjkPlayer || child != rnAvCapture) {
                } else {
                    if (child.getVisibility() != GONE) {
                        child.layout(0, 0, right - left, bottom - top);
                    }
                }
            }
        }
    }
    

    问题一:

    调试后发现调用addView方法,直播控件与视频播放控件没有渲染出来,进一步调试发现,调用addview之后视频控件本身的onLayout方法没有调用。后来,看资料发现布局的构造方法进行addView方法之后,React自动调用onLayout,但是后面进行调用addView的话会进行被React拦截了,需要手动调用layout方法,这里说明下调用view.layout(left,top,right,bottom)方法自动调用view的onLayout方法。

        RNLiveView.this.addView(rnAvCapture);
        rnAvCapture.layout(0, 0, mScreenWidth, mScreenHeight);

    问题二:

    后面遇到播放控件中发现其测量方法没有被调用,导致后续onLayout等方法无法调用,手动调用测量方法。

    总结下:绘制控件步骤:测量控件的大小=》设置控件摆放的位置(left,top,right,bottom)=>绘制控件,不论是任何系统都需要进行的过程,因此,控件没有出现,从这三个方法分析。

     RNLiveView.this.addView(rnIjkPlayer);
     RNLiveView.this.measureChildren(-2147483108, -2147483108);
     rnIjkPlayer.layout(0, 0, mScreenWidth, 400);

    2. 控件切换优化

    从直播切换到播放控件的期间,发现几个问题:一个是updateprops出错,一个是上传控制按钮不见了。

    updateprops出错:

    1.RNLiveViewManager中设置提供给导出给外部属性方法是同步的,比如从直播切换到播放控件的时候两个属性需要更新,一个是mode:设置成播放状态,另一个是url:设置成播放地址,因此要不是mode改了url没改变或者相反,而且会调用两次添加播放控件的方法,需要改成异步,设置完属性再去调用添加控件。引入handler机制并设置开关,一旦调用添加控件的过程未结束,那么后续拦截。

        private void updateLivePlayerAsync() {
            if (mUpdateLiveView)
                return;
    
            if (mHandler == null) {
                mHandler = new Handler() {
                    public void handleMessage(android.os.Message msg) {
                        mUpdateLiveView = false;
                        //业务处理
                    }
                };
            }
            mUpdateLiveView = true;
            mHandler.sendEmptyMessage(this.mMode);
    

    上传控制按钮不见了:

    后面发现是被叠加了,也就是视频播放控件后面添加的因此处于最上层,类似css中的z-index属性,坐标轴中的z轴,查文档发现addView之后会回调onViewAdded()方法,翻译下控件已经添加了,那么这里重新设置z-index的值,需要进行异步。

     private void updateZOrder() {
            final int count = getChildCount();
            for (int i = count - 1; i >= 0; --i) {
                final View child = getChildAt(i);
                if (child != rnIjkPlayer || child != rnAvCapture) {
                    bringChildToFront(child);
                }
            }
        }
    
        private Handler mZOrderHandler = null;
        private Runnable mZOrderRunnable = null;
    
        private void updateZOrderLater() {
            if (mZOrderRunnable != null)
                return;
    
            if (mZOrderHandler == null) {
                mZOrderHandler = new Handler();
            }
    
            mZOrderRunnable = new Runnable() {
                @Override
                public void run() {
                    updateZOrder();
                    mZOrderRunnable = null;
                }
            };
    
            mZOrderHandler.postDelayed(mZOrderRunnable, 200);
        }

    3. 直播视频控件demo

    public class RNLiveView extends FrameLayout {
        private final int mScreenWidth;
        private final int mScreenHeight;
        private RNIjkPlayer rnIjkPlayer;
        private RNAvCapture rnAvCapture;
        private final Context mConntext;
        private String mUrl = "";
        private boolean mUpdateLiveView = false;
        private Handler mHandler;
        private int mMode=0;
    
        public RNLiveView(@NonNull Context context) {
            super(context);
            this.mConntext = context;
            WindowManager wm = (WindowManager) getContext()
                    .getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics dm = new DisplayMetrics();
            wm.getDefaultDisplay().getMetrics(dm);
            //窗口的宽度
            mScreenWidth = dm.widthPixels;
            //窗口高度
            mScreenHeight = dm.heightPixels;
        }
    
    
        public void setUrl(String url) {
            if (mUrl != null && mUrl.compareTo(url) == 0)
                return;
            this.mUrl = url;
        }
    
        public void setFacing(int pos) {
            if (rnAvCapture != null)
                rnAvCapture.setFacing(pos);
        }
    
        private RNAvCapture getRNAvCapture() {
            return new  RNAvCapture(mConntext);
    //        if(rnAvCapture==null)
    //            rnAvCapture=new RNAvCapture(mConntext);
    //        return rnAvCapture;
        }
    
        //设置3种模式:停止,直播发布,视频播放
        public void setMode(int mode) {
            if (mMode != mode) {
                this.mMode=mode;
                updateLivePlayerAsync();
            }
        }
    
        private void updateLivePlayerAsync() {
            if (mUpdateLiveView)
                return;
    
            if (mHandler == null) {
                mHandler = new Handler() {
                    public void handleMessage(android.os.Message msg) {
                        mUpdateLiveView = false;
                        Toast.makeText(mConntext,"what:"+msg.what+",url:"+mUrl,Toast.LENGTH_LONG).show();
                        //停止
                        if (msg.what == 0) {
                            if (rnAvCapture != null) {
                                rnAvCapture.setStart(false);
                            }
                        }   //直播发布
                        else if (msg.what == 1) {
                            try {
                                if (rnIjkPlayer != null)
                                {
                                    RNLiveView.this.removeView(rnIjkPlayer);
                                }
                                rnAvCapture = getRNAvCapture();
                                rnAvCapture.setUrl(mUrl);
                                rnAvCapture.setStart(true);
                                RNLiveView.this.addView(rnAvCapture);
                                rnAvCapture.layout(0, 0, mScreenWidth, mScreenHeight);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }  //视频播放
                        else if (msg.what == 2) {
                            try {
                                if (rnAvCapture != null) {
                                    rnAvCapture.setStart(false);
                                    RNLiveView.this.removeView(rnAvCapture);
                                }
                                rnIjkPlayer = RNIjkPlayer.getInstance(mConntext);
                                rnIjkPlayer.setUrl(mUrl);
                                rnIjkPlayer.setLive(false);
                                rnIjkPlayer.setFullScreen(false);
                                rnIjkPlayer.setIsMediaControl(false);
                                RNLiveView.this.addView(rnIjkPlayer);
                                RNLiveView.this.measureChildren(-2147483108, -2147483108);
                                rnIjkPlayer.layout(0, 0, mScreenWidth, 400);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                };
            }
            mUpdateLiveView = true;
            mHandler.sendEmptyMessage(this.mMode);
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    
        @Override
        public void onViewAdded(View child) {
            super.onViewAdded(child);
            updateZOrderLater();
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            updateZOrderLater();
        }
    
    
        @Override
        protected void onAttachedToWindow() {
            super.onAttachedToWindow();
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            this.removeAllViews();
        }
    
        private void updateZOrder() {
            final int count = getChildCount();
            for (int i = count - 1; i >= 0; --i) {
                final View child = getChildAt(i);
                if (child != rnIjkPlayer || child != rnAvCapture) {
                    bringChildToFront(child);
                }
            }
        }
    
        private Handler mZOrderHandler = null;
        private Runnable mZOrderRunnable = null;
    
        private void updateZOrderLater() {
            if (mZOrderRunnable != null)
                return;
    
            if (mZOrderHandler == null) {
                mZOrderHandler = new Handler();
            }
    
            mZOrderRunnable = new Runnable() {
                @Override
                public void run() {
                    updateZOrder();
                    mZOrderRunnable = null;
                }
            };
    
            mZOrderHandler.postDelayed(mZOrderRunnable, 200);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            final int count = getChildCount();
            for (int i = 0; i < count; i++) {
                final View child = getChildAt(i);
                if (child != rnIjkPlayer || child != rnAvCapture) {
                } else {
                    if (child.getVisibility() != GONE) {
                        child.layout(0, 0, right - left, bottom - top);
                    }
                }
            }
        }
    
    
    }
    

    4效果图

      

  • 相关阅读:
    Spring 整合Struts2 + hibernate
    回调机制
    Struts2配置RESULT中TYPE的参数说明
    hibernate:通用DAO+动态生成HQL语句
    java API chm html 1.5 1.6 中文版英文版 帮助文档
    generator class中各项值的作用及定义
    Hibernate 中 set 里的属性及定义
    一个人的前端项目,踩过的那些坑,一一道来。
    哪些个在 Sublime Text 下,"任性的" 好插件!
    深度挖掘,Html5的 Range 滑动刻度的坑,兼容全平台,将任性进行到底!
  • 原文地址:https://www.cnblogs.com/lmf-techniques/p/6972790.html
Copyright © 2011-2022 走看看