带着问题写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效果图

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏快乐八哥

Cmd Markdown编辑器简明语法手册

标签: Cmd-Markdown 1. 斜体和粗体 使用 * 和 ** 表示斜体和粗体。 示例: 这是 斜体,这是 粗体。 2. 分级标题 使用 === 表示一...

22670
来自专栏Android 开发学习

NestedScrollView 嵌套 ListView 实现滑动折叠效果

83750
来自专栏老司机的简书

老司机带你走进Core Animation 之CAAnimation

开玩笑的,前段时间ipv6被拒啊,超级悲剧的,前后弄了好久,然后需求啊什么的又超多,所以写好的东西也没有时间整理。不过既然我现在回来了,那么这将是一个井喷的时节...

24020
来自专栏LeoXu的博客

Android 拍照功能的开发 原

业务场景是:点击界面(HTML5)上的拍照按钮会调用拍照的JS API,获取其返回照片文件的存储路径、扩展名以及照片文件的Base64字符串,然后在界面上显示图...

12840
来自专栏Android知识点总结

D1-从N角星开始论述自定义控件

2.自定义控件的绘制流程都基于我的这个库,详见:开源计划之--Android绘图库--LogicCanvas

12240
来自专栏前端新视界

给单元素艺术添加动画

原文:Animating Single Div Art 翻译:nzbin 导读:学习工具的最好的方法就是尝试新技术,本文通过“单元素艺术”介绍了 CSS 变量的...

23950
来自专栏非著名程序员

Android自定义 View 实战之 StickerView

本篇文章为利用Matrix自定义View三部曲的第一部曲。 虽然Android内置了许多View供开发者组合和使用,但其多样性还是不足,在很多场景或功能需求下...

24990
来自专栏青玉伏案

iOS开发之各种动画各种页面切面效果

注:其中有些效果调用了CATransition的Private API, 仅供娱乐。 补充:还是有好多小伙伴问那些可以在AppStore中使用,调用私有API的...

223100
来自专栏Windows Community

New UWP Community Toolkit - Carousel

概述 New UWP Community Toolkit  V2.2.0 的版本发布日志中提到了 Carousel 的调整,本篇我们结合代码详细讲解  Caro...

42260
来自专栏Android小菜鸡

定制View的动画

  在日常开发中,View单调的出场和切换已经不能满足用户的要求了。平花的切换动画、出场动画以及离场动画有了一定的必要性。这里就简单的介绍一下View动画的定制...

10140

扫码关注云+社区

领取腾讯云代金券