最近在做的采用React Native项目有一个需求,视频直播与直播流播放同一个布局中,带着问题去思考如何实现,能更容易找到问题关键点,下面分析这个控件解决方法:
现在条件:视频播放控件(开源的ijkplayer),直播控件(自定义控件继承自TextureView与SurfaceView)
1.两种控件切换方式?
讲到切换方式,那应该是从一个布局切换到另一个布局,那如何进行布局,可以是两种布局:嵌套布局(直播控件包括播放控件),单独布局(先移除容器的控件后添加所需控件),采用第二种方式进行实现。
2.如何实现原生控件?
demo的基本功能包括推流,结束推流,播放直播流,前后摄像头切换。
实现控件需要申明两个基本的类:RNLiveViewManager(直播布局管理类)与RNLiveView(直播布局类)
一 RNLiveViewManager
原生视图需要被一个ViewManager
的派生类(或者更常见的,SimpleViewManage
的派生类)创建和管理。一个SimpleViewManager
可以用于这个场景,是因为它能够包含更多公共的属性,譬如背景颜色、透明度、Flexbox布局等等。
提供原生视图很简单:
- 创建一个ViewManager的子类。
- 实现
createViewInstance
方法。 - 导出视图的属性设置器:使用
@ReactProp
(或@ReactPropGroup
)注解。 - 把这个视图管理类注册到应用程序包的
createViewManagers
里。 - 实现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效果图