最近在做的采用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
是这个视图管理类所管理的对象类型,这应当是一个自定义的原生视图。getName
方法返回的名字会用于在JavaScript端引用这个原生视图类型。
public class RNLiveViewManager extends ViewGroupManager {
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 createViewManagers(ReactApplicationContext reactContext) {
return Arrays.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 (
{this.renderChildren()}
);
}
});
//设置导出的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效果图