最近在做的采用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效果图

  

带着问题写React Native原生控件--Android视频直播控件的更多相关文章

  1. [RN] React Native 仿美团下拉筛选菜单控件

    React Native 仿美团下拉筛选菜单控件 演示效果如下: 使用方法如下: 1.安装 npm install react-native-dropdownmenus --save react-na ...

  2. 你不可不知的 React Native 混合用法(Android 篇)

    前言 当前 React Native 虽说版本更新比较快,各种组件也提供的很全面了,但是在某些情况下,混合开发的方式才会快速缩短开发周期,原因无非就是原生平台的"底蕴"无疑更深,拥 ...

  3. React Native常用组件在Android和IOS上的不同

    React Native常用组件在Android和IOS上的不同 一.Text组件在两个平台上的不同表现 1.1 height与fontSize 1.1.1只指定font,不指定height 在这种情 ...

  4. [RN] React Native 键盘管理 在Android TextInput遮盖,上移等问题解决办法

    React Native 键盘管理 在Android TextInput遮盖,上移等问题解决办法 解决办法: 打开android工程,在AndroidManifest.xml中配置如下: <ac ...

  5. React Native 教程:001 - 如何运行官方控件示例 App

    原文发表于我的技术博客 本文主要讲解了如何运行 React Native 官方控件示例 App,包含了一些 React Native 的基础知识以及相关环境的配置. 原文发表于我的技术博客 React ...

  6. React Native环境搭建以及几个基础控件的使用

    之前写了几篇博客,但是没有从最基础的开始写,现在想了想感觉不太合适,所以现在把基础的一些东西给补上,也算是我从零开始学习RN的经验吧! 一.环境搭建 首先声明一下,本人现在用的编辑器是SublimeT ...

  7. NodeJS笔记(五) 使用React Native 创建第一个 Android APP

    参考:原文地址 几个月前官方推出了快速创建工具包,由于对React Native不熟悉这里直接使用这2个工具包进行创建 1. create-react-native-app(下文简称CRNA): 2. ...

  8. react native原生模块引用本地jar包

    比如module目录结构是这样的: 然后libs中的目录是这样的: 只要在build.gradle中加入这段代码就行了 sourceSets { main { manifest.srcFile 'An ...

  9. React Native原生模块向JS传递数据的几种方式(Android)

    一般情况可以分为三种方式: 1. 通过回调函数Callbacks的方式 2. 通过Promises的异步的方式 3. 通过发送事件的事件监听的方式. 参考文档:传送门

随机推荐

  1. git学习(一):建立本地仓库和基本命令

    前沿 最近一直在做目标跟踪,开始一直是通过文件按日期命名的方式来区分版本的,实在是太麻烦了,现在下定决心学习一下git命令 基本概念 集中式:有一台中央服务器,每个人把需要改的部分拿回去改完再送回来 ...

  2. GridView在ScrollView中实现在家更多

    这个本身会有bug  应该在滑动监听中作出判断的 <?xml version="1.0" encoding="utf-8"?><Relativ ...

  3. linux专题一之文件管理(目录结构、创建、查看、删除、移动)

    在linux系统中一切都是文件./ 在linux中为根目录,是一切文件的根目录.本文将通过linux系统的目录结构和与linux文件操作有关的相关命令(touch.mkdir.cp.mv.mv.les ...

  4. 【BZOJ-1597】土地购买 DP + 斜率优化

    1597: [Usaco2008 Mar]土地购买 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 2931  Solved: 1091[Submit] ...

  5. wchar_t 和 char 之间转换

    vc++2005以后,Visual studio 编译器默认的字符集为Unicode.VC中很多字符处理默认为宽字符wchar_t,如CString的getBuffer(),而一些具体操作函数的输入却 ...

  6. spring html5 拖拽上传多文件

    注:这仅仅是一个粗略笔记.有些代码可能没用.兴许会再更新一个能够使用的版本号.不足之处,敬请见谅. 1.spring环境搭建,这里使用的是spring3的jar,须要同一时候引入common-IO 和 ...

  7. ASP.NET MVC 理解MVC模式

    ASP.NET MVC 理解MVC模式 PS:MVC出来很久了,工作上一直没机会用,所以我也没去学.出于兴趣,工作之余我将展开对MVC的深入学习,通过博文来记录所学所得,并希望能得到各位园友的斧正. ...

  8. Android学习基础之onSaveInstanceState和onRestoreInstanceState触发的时机

    先看Application Fundamentals上的一段话:    Android calls onSaveInstanceState() before the activity becomes ...

  9. 《C#从现象到本质》读书笔记(五)第5章字符串第6章垃圾回收第7章异常与异常处理

    <C#从现象到本质>读书笔记(五)第5章字符串 字符串是引用类型,但如果在某方法中,将字符串传入另一方法,在另一方法内部修改,执行完之后,字符串的只并不会改变,而引用类型无论是按值传递还是 ...

  10. The last packet successfully received from the server was 20,519 milliseconds ago. The last packet sent successfully to the server was 0 milliseconds ago.

    本地升级了下MySQL的版本,从5.6升为5.7,数据文件直接拷贝的,项目查询数据库报错: Could not retrieve transation read-only status server ...