美文网首页多媒体科技
React Native播放SDK项目实践

React Native播放SDK项目实践

作者: 金山视频云 | 来源:发表于2017-12-06 16:12 被阅读182次

    1. React Native介绍

    React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架, 支持iOSandroid两大平台,使用Javascript语言来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。React Native的优势:
    (1)由于React Native提供的组件是对原生API的暴露。虽然我们是用Javascript写的代码,但实际上调用的是原生API,原生的UI组件。iOS上调用的是Objective-C代码,Android上调用的是Java代码,所以性能上足以媲美原生应用。
    (2)节省开发成本,90%多界面可以通过React Native开发,一份代码同时可以适配AndroidiOS

    2. 封装RN原生播放器器的缘由

    因为React Native并没有给我们提供播放组件,但是我们在RN里调用播放器还是有两种方法:一种是利用WebView,一种封装RN原生播放器。WebView这里我们就不讨论了,我们讨论一下RN原生播放器的现况,在react-native-community社区有react-native-video原生组件,但是由于在Android平台它是基于MediaPlayer封装的,在iOS平台是基于AVPlayer封装的,对于很多网络协议、视频编码格式并不支持,所以金山云多媒体团队封装了一个KSYVideo,基于KSYMediaPlayer_AndroidKSYMediaPlayer_iOS 播放内核,
    RN原生封装包括JS接口代码以及对应的原生代码(Java/Objective-C)。

    3. Android封装

    封装RN原生组件,需要继承SimpleViewManagerReactPackage(由com.facebook.react包导入),SimpleViewManager本质上是单例——React Native只会为每个管理器创建一个实例。它创建原生的视图并提供给NativeViewHierarchyManagerNativeViewHierarchyManager则会反过来委托它在需要的时候去设置和更新视图的属性。SimpleViewManager还会代理视图的所有委托,并给JavaScript发回对应的事件,SimpleViewManager起到原生的视图和JavaScript组件过渡的作用。

    SimpleViewManager类需要传入一个泛型,该泛型继承AndroidView,这里传入的是ReactKSYVideoViewReactKSYVideoView持有一个原生播放组件KSYTextureView。它们怎么调用播放SDK的呢,如下图所示:

    图1. 类图关系
    • ReactKSYVideoView 是对KSYTextureView的简单封装;
    • ReactKSYVideoViewManager主要作用是返回组件的名字,创建组件实例,注册js的事件(回调函数),接收js的命令;
    • ReactKSYVideoPackage主要是打包ReactKSYVideoViewManager,只是创建一个SimpleViewManager实例;
    • 从图中可以看到KSYVideo组件的属性对应到ReactKSYVideoViewManager java类的方法;
    1. ReactKSYVideoViewManager是与JSKSYVideo交互最紧密的一个类,KSYVideopropTypes属性、回调事件都由ReactKSYVideoViewManager来处理。
    public class ReactKSYVideoViewManager extends SimpleViewManager<ReactKSYVideoView> {
    ...
        @Override//返回组件的Name
        public String getName() {
            return REACT_CLASS;
        }
    
        @Override//创建组件实例
        protected ReactKSYVideoView createViewInstance(final ThemedReactContext reactContext) {
            final ReactKSYVideoView mVideoView = new ReactKSYVideoView(reactContext);
            return mVideoView;
        }
    
        @Override//销毁实例
        public void onDropViewInstance(ReactKSYVideoView view) {
            super.onDropViewInstance(view);
            view.cleanupMediaPlayerResources();
            view.Release();
        }
    
        @Override
        @Nullable//注册JS层回调事件
        public Map getExportedCustomDirectEventTypeConstants() {
            MapBuilder.Builder builder = MapBuilder.builder();
            for (ReactKSYVideoView.Events event : ReactKSYVideoView.Events.values()) {
                builder.put(event.toString(), MapBuilder.of("registrationName", event.toString()));
            }
            return builder.build();
        }
    
        @Nullable
        @Override//注册JS命令
        public Map<String, Integer> getCommandsMap() {
            return MapBuilder.of(
                    COMMAND_SAVEBITMAP_NAME, COMMAND_SAVEBITMAP_ID,
                    COMMAND_RECORDVIDEO_NAME, COMMAND_RECORDVIDEO_ID,
                    COMMAND_SAVEVIDEO_NAME, COMMAND_SAVEVIDEO_ID
            );
        }
    
        @Override//处理JS命令
        public void receiveCommand(ReactKSYVideoView video, int commandId, @Nullable ReadableArray args) {
            switch (commandId){
                case COMMAND_SAVEBITMAP_ID:
                    video.saveBitmap();
                    break;
                case COMMAND_RECORDVIDEO_ID:
                    video.reacordVideo();
                    break;
                case COMMAND_SAVEVIDEO_ID:
                    video.saveVideo();
                    break;
                default:
                    break;
            }
        }
    
        @ReactProp(name = PROP_SRC)//通过@ReactProp 关联JS层的propTypes属性
        public void setSource(ReactKSYVideoView videoView, @Nullable ReadableMap src){
            String source = src.getString(PROP_SRC_URI);
            videoView.setDataSource(source);
        }
    ...//其他 @ReactProp属性
    }
    
    • 我们可以通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。该方法有两个参数,第一个参数是泛型View的实例对象,第二个参数是要设置的属性值。方法的返回值类型必须为void,而且访问控制必须被声明为publicJS层组件的每一个属性的设置都会调用Java层对应ReactProp注解的方法;

    • 通过重写getExportedCustomDirectEventTypeConstants()方法,我们可以在Java层注册回调到JS层事件,这样JS层就可以监听到相关事件回调了;

    • 通过重写GetCommandsMap() 注册 JS发送过来的命令,在receiveCommand()方法中去处理该命令;

    1. ReactKSYVideoView类包含一个原生播放组件KSYTextureView,这是真正的播放实例,ReactKSYVideoView只是一个封装,主要是处理ReactKSYVideoViewManager从JS层接收的命令与属性(@ReactProp ),以及向JS层回调事件。
    //java层向js发送消息事件:
    public class ReactKSYVideoView extends RelativeLayout {
    …
        public int setVideoProgress(int currentProgress) {
        long duration = ksyTextureView.getDuration();
        long time = ksyTextureView.getCurrentPosition();
            WritableMap event = Arguments.createMap();
            event.putDouble("duration", duration/1000);//"duration"用于js中的nativeEvent      
            event.putDouble("curtime", time/1000);
            ReactContext reactContext = (ReactContext) getContext();
            reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
                getId(),//native层和js层两个视图会依据getId()而关联在一起
                "onPlayUpProgress",//事件名称
                event//事件携带的数据
             );
         }
    …
    }
    
    

    4. iOS封装

    Android一样,iOS端的原生视图在封装为RN的组件时,需要被一个RCTViewManager的子类来创建和管理。RCTViewManager类实现了原生组件和JavaScript之间的交互。
    提供对原生视图的封装需要一下几步:

    1. 创建一个RCTViewManager的子类;
    2. 在子类的实现中添加RCT_EXPORT_MODULE()宏;
    3. 实现-(UIView *)view方法返回要封装的视图类;
    4. 使用RCT_EXPORT_VIEW_PROPERTY宏添加供JavaScript使用的属性和事件;
    5. 提供JavaScript代码用以调用该组件。

    具体操作可参考 http://reactnative.cn/docs/0.50/native-component-ios.html#content

    KSYMediaPlayer_iOS SDK提供了一个KSYMoviePlayerController的类用于视频的播放和控制,我们的目标是将此类的功能封装为一个RN的组件供JavaScript层调用,具体操作如下:

    1. 创建基类为UIViewRCTKSYVideo
    #import <React/RCTView.h>
    #import <UIKit/UIKit.h>
    
    @interface RCTKSYVideo : UIView
    - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
    @end
    
    
    1. 在类的实现中添加KSYMoviePlayerController类型的成员变量或者属性
    #import <React/RCTBridgeModule.h>
    #import <React/RCTEventDispatcher.h>
    #import "RCTKSYVideo.h"
    #import <KSYMediaPlayer/KSYMediaPlayer.h>
    @implementation RCTKSYVideo {
        KSYMoviePlayerController *_player;
    }
    
    1. RCTKSYVideo类的初始化中创建KSYMoviePlayerController的对象,并将它的view添加为子view,并注册需要监测的通知及处理函数,具体处理方式可参考KSYMediaPlayer_iOS SDK demo中的实现
    - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
        if ((self = [super init])) {
            _player = [[KSYMoviePlayerController alloc]initWithContentURL:nil];
            registeredNotifications = [[NSMutableArray alloc] init];
            [self setupObservers:_player];
            [_player addObserver:self forKeyPath:@"currentPlaybackTime" options:nil context:nil];
            [self addSubview:_player.view];
        }
        return self;
    }
    
    - (void)setupObservers:(KSYMoviePlayerController*)player
    {
        [self registerObserver:MPMediaPlaybackIsPreparedToPlayDidChangeNotification player:player];
        [self registerObserver:MPMoviePlayerPlaybackStateDidChangeNotification player:player];
        [self registerObserver:MPMoviePlayerPlaybackDidFinishNotification player:player];
        [self registerObserver:MPMoviePlayerLoadStateDidChangeNotification player:player];
        [self registerObserver:MPMovieNaturalSizeAvailableNotification player:player];
        [self registerObserver:MPMoviePlayerFirstVideoFrameRenderedNotification player:player];
        [self registerObserver:MPMoviePlayerFirstAudioFrameRenderedNotification player:player];
        [self registerObserver:MPMoviePlayerSuggestReloadNotification player:player];
        [self registerObserver:MPMoviePlayerPlaybackStatusNotification player:player];
        [self registerObserver:MPMoviePlayerNetworkStatusChangeNotification player:player];
        [self registerObserver:MPMoviePlayerSeekCompleteNotification player:player];
        [self registerObserver:MPMoviePlayerPlaybackTimedTextNotification player:player];
    }
    
    1. 创建基类为RCTViewManagerRCTKSYVideoManager类,添加RCT_EXPORT_MODULE()宏,实现- (UIView *)view方法,使其返回RCTKSYVideo
    #import <React/RCTViewManager.h>
    @interface RCTKSYVideoManager : RCTViewManager
    @end
    
    #import "RCTKSYVideoManager.h"
    #import "RCTKSYVideo.h"
    
    @implementation RCTKSYVideoManager
    RCT_EXPORT_MODULE()
    @synthesize bridge = _bridge;
    - (UIView *)view
    {
        return [[RCTKSYVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
    }
    @end
    
    1. RCTKSYVideoManager.m中使用RCT_EXPORT_VIEW_PROPERTY宏添加属性和事件,RCTBubblingEventBlock类型的属性即为事件属性
    RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
    RCT_EXPORT_VIEW_PROPERTY(seek, float);
    RCT_EXPORT_VIEW_PROPERTY(timeout, NSDictionary);
    RCT_EXPORT_VIEW_PROPERTY(bufferTime, float);
    RCT_EXPORT_VIEW_PROPERTY(bufferSize, float);
    RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
    RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
    RCT_EXPORT_VIEW_PROPERTY(paused, BOOL);
    RCT_EXPORT_VIEW_PROPERTY(muted, BOOL);
    RCT_EXPORT_VIEW_PROPERTY(mirror, BOOL);
    RCT_EXPORT_VIEW_PROPERTY(volume, float);
    RCT_EXPORT_VIEW_PROPERTY(degree, int);
    RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
    
    
    RCT_EXPORT_VIEW_PROPERTY(onVideoTouch, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onVideoError, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onVideoProgress, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onVideoSeek, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onVideoEnd, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTBubblingEventBlock);
    RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTBubblingEventBlock);
    
    1. RCTKSYVideo.m中添加对属性的实现和事件属性的调用
    - (void)setSrc:(NSDictionary *)source {
        NSString *uri = [source objectForKey:@"uri"];
        NSURL* url = [NSURL URLWithString:uri];
        [_player reset:NO];
        [_player setUrl:url];
        [_player prepareToPlay];
        if(self.onVideoLoadStart) {
            self.onVideoLoadStart(@{});
        }
    }
    

    5. JavaScript 实现

    接下来封装JavaScript接口层,可以由JS调用的组件, 大部分过程都由React底层的JavaJavaScript代码来完成,你所需要做的就是通过propTypes来描述属性的类型。

    //KSYVideo 组件
    'use strict';
    import React,{ Component}from 'react';
    import PropTypes from 'prop-types';
    import {
        requireNativeComponent,
        View,
        UIManager,
        findNodeHandle,
        StyleSheet
    }from 'react-native';
    var RCT_VIDEO_REF = 'KSYVideo';
    export default class KSYVideo extends Component {
        constructor(props) {
            super(props);
        }
    
        setNativeProps(nativeProps) {
            this.refs[RCT_VIDEO_REF].setNativeProps(nativeProps);
        }
    
        seek = (time) => {
            this.setNativeProps({ seek: time });
        };
    
        _onTouch = (event)=>{
            if (!this.props.onTouch){
                return;
            }
            this.props.onTouch();
        }
    
        //...//回调函数
    
        render(){
            const source = this.props.source;
            let uri = source.uri;
            const nativeProps = Object.assign({}, this.props);
            Object.assign(nativeProps, {
                
                src: {
                    uri,
                },
                onVideoTouch:this._onTouch,
                onVideoLoadStart: this._onLoadStart,
                onVideoLoad:this._onLoad,
                onVideoEnd:this._onEnd,
                onVideoError:this._onError,
                onVideoProgress:this._onProgress,
                onVideoSeek: this._onSeek,
                onReadyForDisplay: this._onReadyForDisplay,
                onPlaybackStalled: this._onPlaybackStalled,
                onPlaybackResume: this._onPlaybackResume,
            });
    
            return (  
                    <RCTKSYVideo           
                        {/*...nativeProps*/}
                        ref = {RCT_VIDEO_REF}
                    />
            );
        };
    }
    //通过@ReactProp注解对应到原生视图上
    KSYVideo.propTypes = {
        style: View.propTypes.style,
        src: PropTypes.object,
        seek: PropTypes.number,
        onVideoTouch: PropTypes.func,
        onVideoLoadStart: PropTypes.func,
        onVideoLoad: PropTypes.func,
        onVideoEnd: PropTypes.func,
        onVideoError: PropTypes.func,
        onVideoProgress: PropTypes.func,
        onVideoSeek: PropTypes.func,
        //...//其他属性
        //...View.propTypes,
    };
    
    const RCTKSYVideo = requireNativeComponent('RCTKSYVideo',KSYVideo,{
       nativeOnly: {
        src: true,
        seek: true,
      },
    });
    
    
    • JS组件使用的模块是requireNativeComponentrequireNativeComponent的第一个参数是原生视图的名字,而第二个参数是一个描述组件接口的对象,第三个参数可以不使用;
    • KSYVideopropTypes属性,对应到原生视图上,即对应ReactKSYVideoViewManager@ReactProp(name = " ")
    • KSYVideo属性中的PropTypes.fun项,则是Java层回调事件时调用的函数;
    • 有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应JS封装组件的属性,可以使用nativeOnly来声明,例如srcseek

    5. 总结

    欢迎大家试用并提pull request
    Github代码地址:https://github.com/ksvc/react-native-video-player
    npm仓库地址:https://www.npmjs.com/package/react-native-ksyvideo
    初次接触React Native,对播放SDK的封装过程难免有不足的地方,欢迎大家指正。

    转载请注明:
    作者金山视频云,首发简书 Jianshu.com


    欢迎大家试用金山云多媒体SDK:
    https://github.com/ksvc/

    金山云SDK相关的QQ交流群:

    • 视频云技术交流群:574179720

    相关文章

      网友评论

        本文标题:React Native播放SDK项目实践

        本文链接:https://www.haomeiwen.com/subject/dpgdvxtx.html