1. React Native介绍
React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架, 支持iOS和android两大平台,使用Javascript语言来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。React Native的优势:
(1)由于React Native提供的组件是对原生API的暴露。虽然我们是用Javascript写的代码,但实际上调用的是原生API,原生的UI组件。iOS上调用的是Objective-C代码,Android上调用的是Java代码,所以性能上足以媲美原生应用。
(2)节省开发成本,90%多界面可以通过React Native开发,一份代码同时可以适配Android和iOS。
2. 封装RN原生播放器器的缘由
因为React Native并没有给我们提供播放组件,但是我们在RN里调用播放器还是有两种方法:一种是利用WebView
,一种封装RN原生播放器。WebView
这里我们就不讨论了,我们讨论一下RN原生播放器的现况,在react-native-community社区有react-native-video原生组件,但是由于在Android平台它是基于MediaPlayer
封装的,在iOS平台是基于AVPlayer
封装的,对于很多网络协议、视频编码格式并不支持,所以金山云多媒体团队封装了一个KSYVideo
,基于KSYMediaPlayer_Android、KSYMediaPlayer_iOS 播放内核,
RN原生封装包括JS接口代码以及对应的原生代码(Java/Objective-C)。
3. Android封装
封装RN原生组件,需要继承SimpleViewManager
和ReactPackage
(由com.facebook.react包导入),SimpleViewManager
本质上是单例——React Native只会为每个管理器创建一个实例。它创建原生的视图并提供给NativeViewHierarchyManager
,NativeViewHierarchyManager
则会反过来委托它在需要的时候去设置和更新视图的属性。SimpleViewManager
还会代理视图的所有委托,并给JavaScript发回对应的事件,SimpleViewManager
起到原生的视图和JavaScript组件过渡的作用。
SimpleViewManager
类需要传入一个泛型,该泛型继承Android的View
,这里传入的是ReactKSYVideoView
,ReactKSYVideoView
持有一个原生播放组件KSYTextureView。它们怎么调用播放SDK的呢,如下图所示:
-
ReactKSYVideoView
是对KSYTextureView
的简单封装; -
ReactKSYVideoViewManager
主要作用是返回组件的名字,创建组件实例,注册js的事件(回调函数),接收js的命令; -
ReactKSYVideoPackage
主要是打包ReactKSYVideoViewManager
,只是创建一个SimpleViewManager
实例; - 从图中可以看到
KSYVideo
组件的属性对应到ReactKSYVideoViewManager
java类的方法;
-
ReactKSYVideoViewManager
是与JS层KSYVideo
交互最紧密的一个类,KSYVideo
的propTypes
属性、回调事件都由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,而且访问控制必须被声明为public。JS层组件的每一个属性的设置都会调用Java层对应ReactProp注解的方法; -
通过重写
getExportedCustomDirectEventTypeConstants()
方法,我们可以在Java层注册回调到JS层事件,这样JS层就可以监听到相关事件回调了; -
通过重写
GetCommandsMap()
注册 JS发送过来的命令,在receiveCommand()
方法中去处理该命令;
-
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之间的交互。
提供对原生视图的封装需要一下几步:
- 创建一个
RCTViewManager
的子类; - 在子类的实现中添加
RCT_EXPORT_MODULE()
宏; - 实现
-(UIView *)view
方法返回要封装的视图类; - 使用
RCT_EXPORT_VIEW_PROPERTY
宏添加供JavaScript使用的属性和事件; - 提供JavaScript代码用以调用该组件。
具体操作可参考 http://reactnative.cn/docs/0.50/native-component-ios.html#content
KSYMediaPlayer_iOS SDK提供了一个KSYMoviePlayerController
的类用于视频的播放和控制,我们的目标是将此类的功能封装为一个RN的组件供JavaScript层调用,具体操作如下:
- 创建基类为
UIView
的RCTKSYVideo
类
#import <React/RCTView.h>
#import <UIKit/UIKit.h>
@interface RCTKSYVideo : UIView
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
@end
- 在类的实现中添加
KSYMoviePlayerController
类型的成员变量或者属性
#import <React/RCTBridgeModule.h>
#import <React/RCTEventDispatcher.h>
#import "RCTKSYVideo.h"
#import <KSYMediaPlayer/KSYMediaPlayer.h>
@implementation RCTKSYVideo {
KSYMoviePlayerController *_player;
}
-
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];
}
- 创建基类为
RCTViewManager
的RCTKSYVideoManager
类,添加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
- 在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);
- 在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底层的Java和JavaScript代码来完成,你所需要做的就是通过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组件使用的模块是
requireNativeComponent
,requireNativeComponent
的第一个参数是原生视图的名字,而第二个参数是一个描述组件接口的对象,第三个参数可以不使用; -
KSYVideo
的propTypes
属性,对应到原生视图上,即对应ReactKSYVideoViewManager
的@ReactProp(name = " ")
; -
KSYVideo
属性中的PropTypes.fun
项,则是Java层回调事件时调用的函数; - 有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应JS封装组件的属性,可以使用
nativeOnly
来声明,例如src
、seek
;
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
网友评论