RN 和 iOS通信
背景
RN
给我们提供了丰富的UI
库 和组件
库, 但也有需要和native
视图的.比如MapKit
, AVFoundation
. 之类的需求. 因为篇幅有限,本文仅使用部分代码片段用以说明Module
实现的方式,详细代码,请参考react-native-wmm-aliyunplayer
react-native-wmm-aliyunplayer
下面以本库代述。
本库主要是对react-native-lewin-aliyunplayer
的扩展,以及对阿里云播SDK做了下载模块的拓展,非阿里云播器功能的全量封装。本文仅以此案例用于阐述 Native Module的封装以及,发布npm js 公仓的表述。
环境
RN依赖版本
// package.json
{
"dependencies": {
"react": "16.13.1",
"react-native": "0.63.3",
}
}
node/npm/cocoapods 版本
jeversonjee@macbookpro ~ % npm -v
8.5.0
jeversonjee@macbookpro ~ % node -v
v16.14.2
jeversonjee@macbookpro~ % pod --version
1.11.3
应用场景
音频锁屏播放
在音视频播放的需求中,我们通常会有在锁屏实现音频播放的功能;在react-native 0.63
没有提供该组件的情况下,我们需要原生来实现,并和native
通信,那么和原生通信rn
是提供iOS模块和Android模块详细api请点击上述链接查看
原理
基于KVO
的方式NSNofitificationCenter
多任务下载
- 多任务下载,和页面显示无关,因此,选用
NativeModule
来实现。 - 支持原生
阿里云播SDK
的方法- 删除下载文件
- 销毁下载对象
- 使用AVPVidStsSource准备播放
- 设置下载的保存路径
- 开始下载
- 设置下载config
- 设置下载的trackIndex
- 获取SDK版本号信息
- 下载任务回调事件监听
- 错误代理回调
- 下载文件的处理进度回调
- 下载完成回调
代码示例
采用我们团队的npm公仓库react-native-wmm-aliyunplayer
iOS
Code Slice Powered by nbhhcty
根据上述的业务需求,我们了解到原生模块需要创建RNAliMediaDownloader
集成于RCTEventEmitter
遵循了 RCTBridgeModule
. 官方文档对于事件监听即RCTEventEmmitter
有详细的描述,此处主要是方变我们发送订阅事件消息
以及声明支持的事件类型
。关于RCTBridgeModule
主要是方便前端
与RNModule
通信时声明的宏方法定义RCT_EXPORT_METHOD
, 关于暴露方法的宏定义,官方文档比较详细,本文不在对此功能赘述。关于文章中使用RCTLog
而非NSLog 主要是方便,前端Log debug 打印。Lib 中推荐使用RCTLog
iOS 声明
//
// RNAliMediaDownloader.h
// AliyunVideoClient_Entrance
//
// Created by wsk on 2022/12/6.
// Copyright © 2022 Aliyun. All rights reserved.
//
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
extern NSString* const _Nullable EK_downloaderOnPrepared;
extern NSString* const _Nullable EK_downloaderOnError;
extern NSString* const _Nullable EK_downloaderOnDownloadingProgress;
extern NSString* const _Nullable EK_downloaderOnProcessingProgress;
extern NSString* const _Nullable EK_downloaderOnCompletion;
NS_ASSUME_NONNULL_BEGIN
@interface RNAliMediaDownloader : RCTEventEmitter<RCTBridgeModule>
@end
NS_ASSUME_NONNULL_END
iOS 实现部分
//
// RNAliMediaDownloader.m
// AliyunVideoClient_Entrance
//
// Created by wsk on 2022/12/6.
// Copyright © 2022 Aliyun. All rights reserved.
//
#import "RNAliMediaDownloader.h"
#import <AliyunMediaDownloader/AliyunMediaDownloader.h>
#import <React/RCTLog.h>
#import "AliMediaDownloader+vid.h"
NSString* const EK_downloaderOnPrepared = @"downloaderOnPrepared";
NSString* const EK_downloaderOnError = @"downloaderOnError";
NSString* const EK_downloaderOnDownloadingProgress = @"downloaderOnDownloadingProgress";
NSString* const EK_downloaderOnProcessingProgress = @"downloaderOnProcessingProgress";
NSString* const EK_downloaderOnCompletion = @"downloaderOnCompletion";
@interface RNAliMediaDownloader ()
@property (nonatomic,strong)NSMutableDictionary<NSString*, AliMediaDownloader*> *preDownloaders;
@property (nonatomic,strong)NSMutableDictionary<NSString*, AliMediaDownloader*> *downloaders;
@end
@implementation RNAliMediaDownloader
RCT_EXPORT_MODULE(RNAliMediaDownloader)
/**
@brief 初始化下载对象
*/
- (instancetype)init {
if (self = [super init]) {
}
return self;
}
-(NSArray<NSString *> *)supportedEvents{
return @[
EK_downloaderOnPrepared,
EK_downloaderOnError,
EK_downloaderOnDownloadingProgress,
EK_downloaderOnProcessingProgress,
EK_downloaderOnCompletion
];
}
- (NSDictionary *)constantsToExport {
return @{
@"onPrepared": EK_downloaderOnPrepared,
@"onError": EK_downloaderOnError,
@"onProcessingProgress": EK_downloaderOnProcessingProgress,
@"onDownloadingProgress": EK_downloaderOnDownloadingProgress,
@"onCompletion": EK_downloaderOnCompletion
};
}
- (NSString*)mapKey:(NSString*)vid trackIndex:(int)trackIndex {
NSString *key = [NSString stringWithFormat:@"%@-%@", vid, @(trackIndex)];
return key;
}
/**
@brief 删除下载文件
@param saveDir 文件保存路径
@param vid vid
@param format 格式
@param index vid对应的下载索引
*/
RCT_EXPORT_METHOD(deleteFile:(NSString*)saveDir
vid:(NSString*)vid
format:(NSString*)format
index:(int)index
callback:(RCTResponseSenderBlock)callback) {
RCTLogInfo(@"rn_media_downloader deleteFile vid = %@ trackIndex = %d", vid, index);
int result = [AliMediaDownloader deleteFile:saveDir vid:vid format:format index:index];
callback(@[[NSNumber numberWithInt:result]]);
/// 如果是准备中、下载中删除
NSString *key = [self mapKey:vid trackIndex:index];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
if (downloader) {
RCTLogInfo(@"rn_media_downloader deleteFile downloaders");
[downloader deleteFile];
[downloader destroy];
self.downloaders[key] = nil;
}
downloader = [self.preDownloaders objectForKey:vid];
if (downloader) {
RCTLogInfo(@"rn_media_downloader deleteFile preDownloaders");
[downloader destroy];
self.preDownloaders[vid] = nil;
}
}
/**
@brief 销毁下载对象
*/
RCT_EXPORT_METHOD(destroyWithVid:(NSString*)vid trackIndex:(int)trackIndex) {
RCTLogInfo(@"rn_media_downloader destroyWithVid vid = %@ trackIndex = %d", vid, trackIndex);
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
if (downloader) {
[downloader destroy];
self.downloaders[key] = nil;
}
downloader = [self.preDownloaders objectForKey:vid];
if (downloader) {
[downloader destroy];
self.preDownloaders[vid] = nil;
}
}
/**
@brief 使用AVPVidStsSource准备播放
@param source vid sts的播放方式
*/
RCT_EXPORT_METHOD(prepareWithVid:(NSString*)vid
accessKeyId:(NSString *) accessKeyId
accessKeySecret:(NSString *) accessKeySecret
securityToken:(NSString *) securityToken
region:(NSString *) region) {
RCTLogInfo(@"rn_media_downloader prepareWithVid vid = %@", vid);
AVPVidStsSource *source = [[AVPVidStsSource alloc] initWithVid:vid
accessKeyId:accessKeyId
accessKeySecret:accessKeySecret
securityToken:securityToken
region:region];
AliMediaDownloader * downloader = [[AliMediaDownloader alloc] init];
downloader.mm_vid = vid;
downloader.delegate = (id<AMDDelegate>)self;
self.preDownloaders[downloader.mm_vid] = downloader;
[downloader prepareWithVid:source];
}
/**
@brief 鉴权过期,更新AVPVidStsSource信息,
@param source vid sts的信息
*/
RCT_EXPORT_METHOD(updateWithVid:(NSString*)vid
trackIndex:(int) trackIndex
accessKeyId:(NSString *) accessKeyId
accessKeySecret:(NSString *) accessKeySecret
securityToken:(NSString *) securityToken
region:(NSString *) region) {
RCTLogInfo(@"rn_media_downloader updateWithVid vid = %@ trackIndex = %d", vid, trackIndex);
AVPVidStsSource *source = [[AVPVidStsSource alloc] initWithVid:vid
accessKeyId:accessKeyId
accessKeySecret:accessKeySecret
securityToken:securityToken
region:region];
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
[downloader updateWithVid:source];
}
/**
@brief 设置下载的保存路径
@param dir 保存文件夹
*/
RCT_EXPORT_METHOD(setSaveDirectory:(NSString*)dir vid:(NSString*)vid trackIndex:(int)trackIndex) {
RCTLogInfo(@"rn_media_downloader setSaveDirectory vid = %@ trackIndex = %d", vid, trackIndex);
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
[downloader setSaveDirectory:dir];
}
/**
@brief 开始下载
*/
RCT_EXPORT_METHOD(startWithVid:(NSString*)vid trackIndex:(int)trackIndex) {
RCTLogInfo(@"rn_media_downloader startWithVid vid = %@ trackIndex = %d", vid, trackIndex);
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
[downloader start];
}
/**
@brief 停止下载
*/
RCT_EXPORT_METHOD(stopWithVid:(NSString*)vid trackIndex:(int)trackIndex) {
RCTLogInfo(@"rn_media_downloader stopWithVid vid = %@ trackIndex = %d", vid, trackIndex);
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
[downloader stop];
}
/**
@brief 获取下载config
*/
RCT_EXPORT_METHOD(getConfigWithVid:(NSString*)vid trackIndex:(int)trackIndex callback:(RCTPromiseResolveBlock)callback) {
RCTLogInfo(@"rn_media_downloader getConfigWithVid vid = %@ trackIndex = %d", vid, trackIndex);
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
AVDConfig *config = [downloader getConfig];
NSMutableDictionary *dic = [NSMutableDictionary dictionary];
dic[@"timeoutMs"] = @(config.timeoutMs);
dic[@"connnectTimoutMs"] = @(config.connnectTimoutMs);
dic[@"referer"] = config.referer;
dic[@"userAgent"] = config.userAgent;
dic[@"httpProxy"] = config.httpProxy;
callback(dic);
}
/**
@brief 设置下载config
*/
RCT_EXPORT_METHOD(setConfig:(int)timeoutMs
connnectTimoutMs:(int)connnectTimoutMs
referer:(NSString*)referer
userAgent:(NSString*)userAgent
httpProxy:(NSString*)httpProxy
vid:(NSString*)vid
trackIndex:(int)trackIndex
) {
RCTLogInfo(@"rn_media_downloader setConfig vid = %@ trackIndex = %d", vid, trackIndex);
AVDConfig *config = [[AVDConfig alloc] init];
config.timeoutMs = timeoutMs;
config.connnectTimoutMs = connnectTimoutMs;
config.referer = referer;
config.userAgent = userAgent;
config.httpProxy = httpProxy;
NSString *key = [self mapKey:vid trackIndex:trackIndex];
AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
[downloader setConfig:config];
}
/**
@brief 设置下载的trackIndex
@param trackIndex 从prepare回调中可以获取所有index
*/
RCT_EXPORT_METHOD(selectTrack:(int)trackIndex vid:(NSString*)vid) {
RCTLogInfo(@"rn_media_downloader selectTrack vid = %@ trackIndex = %d", vid, trackIndex);
AliMediaDownloader *downloader = self.preDownloaders[vid];
downloader.mm_trackIndex = @(trackIndex).stringValue;
[downloader selectTrack:trackIndex];
NSString *key = [self mapKey:vid trackIndex:trackIndex];
self.downloaders[key] = downloader;
self.preDownloaders[vid] = nil;
}
/**
@brief 获取SDK版本号信息
*/
RCT_EXPORT_METHOD(getSDKVersion:(RCTPromiseResolveBlock)callback) {
RCTLogInfo(@"rn_media_downloader getSDKVersion");
NSString *version = [AliMediaDownloader getSDKVersion];
callback(version);
}
- (NSMutableDictionary<NSString*,AliMediaDownloader*> *)downloaders {
if(!_downloaders) {
_downloaders = [[NSMutableDictionary<NSString*,AliMediaDownloader*> alloc] init];
}
return _downloaders;
}
- (NSMutableDictionary<NSString*,AliMediaDownloader*> *)preDownloaders {
if(!_preDownloaders) {
_preDownloaders = [[NSMutableDictionary<NSString*,AliMediaDownloader*> alloc] init];
}
return _preDownloaders;
}
@end
#pragma mark - <AMDDelegate>
@interface RNAliMediaDownloader (AMDDelegate)<AMDDelegate>
@end
@implementation RNAliMediaDownloader (AMDDelegate)
/**
@brief 下载准备完成事件回调
@param downloader 下载downloader指针
@param info 下载准备完成回调,@see AVPMediaInfo
*/
-(void)onPrepared:(AliMediaDownloader*)downloader mediaInfo:(AVPMediaInfo*)info {
RCTLogInfo(@"rn_media_downloader onPrepared vid = %@", downloader.mm_vid);
NSMutableArray *tracks = [NSMutableArray array];
[info.tracks enumerateObjectsUsingBlock:^(AVPTrackInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSMutableDictionary *track = [NSMutableDictionary dictionary];
track[@"trackIndex"] = @(obj.trackIndex);
track[@"trackDefinition"] = obj.trackDefinition;
track[@"vodFormat"] = obj.vodFormat;
track[@"vodFileSize"] = @(obj.vodFileSize);
[tracks addObject:track];
}];
NSMutableDictionary *body = [NSMutableDictionary dictionary];
body[@"vid"] = downloader.mm_vid;
body[@"tracks"] = tracks;
[self sendEventWithName:EK_downloaderOnPrepared body:body];
}
/**
@brief 错误代理回调
@param downloader 下载downloader指针
@param errorModel 播放器错误描述,参考AliVcPlayerErrorModel
*/
- (void)onError:(AliMediaDownloader*)downloader errorModel:(AVPErrorModel *)errorModel {
RCTLogInfo(@"rn_media_downloader onError vid = %@ code = %@", downloader.mm_vid, @(errorModel.code));
NSMutableDictionary *body = [NSMutableDictionary dictionary];
body[@"vid"] = downloader.mm_vid;
body[@"trackIndex"] = downloader.mm_trackIndex;
body[@"code"] = @(errorModel.code);
body[@"message"] = errorModel.message;
[self sendEventWithName:EK_downloaderOnError body:body];
}
/**
@brief 下载进度回调
@param downloader 下载downloader指针
@param percent 下载进度 0-100
*/
- (void)onDownloadingProgress:(AliMediaDownloader*)downloader percentage:(int)percent {
RCTLogInfo(@"rn_media_downloader onDownloadingProgress vid = %@-%@ percent = %d", downloader.mm_vid, downloader.mm_trackIndex, percent);
NSMutableDictionary *body = [NSMutableDictionary dictionary];
body[@"vid"] = downloader.mm_vid;
body[@"trackIndex"] = downloader.mm_trackIndex;
body[@"percent"] = @(percent);
[self sendEventWithName:EK_downloaderOnDownloadingProgress body:body];
}
/**
@brief 下载文件的处理进度回调
@param downloader 下载downloader指针
@param percent 下载进度 0-100
*/
- (void)onProcessingProgress:(AliMediaDownloader*)downloader percentage:(int)percent {
RCTLogInfo(@"rn_media_downloader onProcessingProgress vid = %@-%@ percent = %d", downloader.mm_vid, downloader.mm_trackIndex, percent);
NSMutableDictionary *body = [NSMutableDictionary dictionary];
body[@"vid"] = downloader.mm_vid;
body[@"trackIndex"] = downloader.mm_trackIndex;
body[@"percent"] = @(percent);
[self sendEventWithName:EK_downloaderOnProcessingProgress body:body];
}
/**
@brief 下载完成回调
@param downloader 下载downloader指针
*/
- (void)onCompletion:(AliMediaDownloader*)downloader {
RCTLogInfo(@"rn_media_downloader onCompletion vid = %@-%@-%@", downloader.mm_vid, downloader.mm_trackIndex, downloader.downloadedFilePath);
NSMutableDictionary *body = [NSMutableDictionary dictionary];
body[@"vid"] = downloader.mm_vid;
body[@"trackIndex"] = downloader.mm_trackIndex;
body[@"filePath"] = [downloader downloadedFilePath];
[self sendEventWithName:EK_downloaderOnCompletion body:body];
}
@end
前端部分
Tallk is cheap show me code
Code slice powered by jeversonjee
import {
NativeModules,
NativeEventEmitter,
Platform,
EmitterSubscription,
} from 'react-native';
import RNFS from 'react-native-fs';
import type { IAssumeRole, ICompletion, IProgress } from './types';
const { RNAliMediaDownloader } = NativeModules;
const RNAliMediaEmitter = new NativeEventEmitter(RNAliMediaDownloader);
const defaultFilePath =
(Platform.OS === 'ios'
? RNFS.DocumentDirectoryPath
: RNFS.ExternalDirectoryPath) + '/media';
export enum EEvent {
ON_PREPARED = RNAliMediaDownloader.onPrepared,
ON_ERROR = RNAliMediaDownloader.onError,
ON_PROCESSING_PROGRESS = RNAliMediaDownloader.onProcessingProgress,
ON_DOWNLOADING_PROGRESS = RNAliMediaDownloader.onDownloadingProgress,
ON_COMPLETION = RNAliMediaDownloader.onCompletion,
}
let subscriptions = new Map<string, EmitterSubscription>();
// 下载进度回调事件
export const onDownloadingProgress = (callBack: (data: IProgress) => void) => {
if (
subscriptions &&
subscriptions.has(String(EEvent.ON_DOWNLOADING_PROGRESS))
) {
console.log('你已经订阅过 onDownloadingProgress');
return;
}
const downloadProcesssSubs = RNAliMediaEmitter.addListener(
String(EEvent.ON_DOWNLOADING_PROGRESS),
(res: IProgress) => {
callBack(res);
}
);
subscriptions.set(
String(EEvent.ON_DOWNLOADING_PROGRESS),
downloadProcesssSubs
);
};
/**
* @description 下载完成回调
*/
export const onDownloaderCompletion = (
callback: (data: ICompletion) => void
) => {
if (subscriptions && subscriptions.has(String(EEvent.ON_COMPLETION))) {
console.log('你已经订阅过 onDownloaderCompletion');
return;
}
const completionSubs = RNAliMediaEmitter.addListener(
String(EEvent.ON_COMPLETION),
(res: ICompletion) => {
callback(res);
}
);
subscriptions.set(String(EEvent.ON_COMPLETION), completionSubs);
};
/**
* @description 下载准备回调
*/
export const onDownloaderPrepared = (callback: (data: any) => void) => {
if (subscriptions && subscriptions.has(String(EEvent.ON_PREPARED))) {
console.log('你已经订阅过 onDownloaderPrepared');
return;
}
const preparedSubs = RNAliMediaEmitter.addListener(
String(EEvent.ON_PREPARED),
(res: any) => {
callback(res);
}
);
subscriptions.set(String(EEvent.ON_PREPARED), preparedSubs);
};
/**
* @description 下载异常回调
*/
export const onDownloaderError = (callback: (data: any) => void) => {
if (subscriptions && subscriptions.has(String(EEvent.ON_PREPARED))) {
console.log('你已经订阅过 onDownloaderError');
return;
}
const errorSubs = RNAliMediaEmitter.addListener(
String(EEvent.ON_ERROR),
(res: any) => {
callback(res);
}
);
subscriptions.set(String(EEvent.ON_ERROR), errorSubs);
};
// 取消监听事件
export const unsubscription = () => {
if (subscriptions && subscriptions.size > 0) {
for (let value of subscriptions.values()) {
value.remove();
}
subscriptions.clear();
}
};
// 设置下载路径
export const setSaveDirectory = (
vid: string,
trackIndex = 0,
filePath?: string
) => {
if (!filePath) filePath = defaultFilePath;
RNAliMediaDownloader.setSaveDirectory(filePath, vid, trackIndex);
};
// 准备下载
export const prepareWithVid = async (vid: string, role: IAssumeRole) => {
RNAliMediaDownloader.prepareWithVid(
vid,
role.AccessKeyId,
role.AccessKeySecret,
role.SecurityToken,
role.Region
);
};
// 开始下载
export const startDownload = async (vid: string, trackIndex = 0) => {
RNAliMediaDownloader.startWithVid(vid, trackIndex);
};
// 暂停下载
export const pauseDownload = (vid: string, trackIndex = 0) => {
RNAliMediaDownloader.stopWithVid(vid, trackIndex);
};
// 删除文件
export const deleteDownload = async (
saveDir: string,
vid: string,
format: string,
index: number
) => {
const influenceRows = await RNAliMediaDownloader.deleteFile(
saveDir,
vid,
format,
index
);
return new Promise((resolve) => {
resolve({ res: influenceRows });
});
};
// 设置分辨率
export const selectTrack = (vid: string, trackIndex = 0) => {
RNAliMediaDownloader.selectTrack(trackIndex, vid);
};
发行、调试、验证
万事俱备,我们实现了原生部分。桥接部分,那如何来创建一个用于发行、调试、验证
的方式,当然不发行也是可以的,可以使用patch
来代替每次手动替换,node_modules 原生声明和桥接目录。因为跨平台调用链很长的问题,所以patch 可以是沟通成本最低的方式,无任何优劣的比较。在此隆重介绍create-react-native-library
create-react-native-library
目录结构
- android 主要存放原生代码逻辑
- doc 主要存放一些关于库前端调用方法,和注释.
主要是让前端了解方法,属性的作用
- example 原生调试、debug 的目录就和
react-native
项目基本无区别。 - lib 为输出目录。主要是src 中声明的前端方法编译成
js
因此处作者采用的是ts
- node_modules 本库 所依赖的一些包。
- src 前端代码,编译前产物。
- leftbook.yml 这是 git pre-commit 的校验配置文件
- ur project name.podSpec 主要是用于声明这个一个pod lib 以及你需要在iOS 中配置的依赖,以及podSpec的所有声明编译方式,都是全量支持的。关于podSpec 配置,请自行了解。
- tsConfig: 关于tsc 的编译配置,关于配置方面的文档请自行了解
BTW
关于一个原生了解一个库,最直接的方式就是通过官方文档,以及本文提示词去搜索。本文还是赘述一下使用方式。
- example 调试目录在二级目录中,可以在lib 目录中执行命令的吗?
- 由于xcode 12 的限制,关于模拟器,虽然我不爱模拟器debug 只需要在podSpec 中指定Target 指定一下 arm64
- 最重要的是看一下项目的 package.json
视屏播放
logs
- 2023.05-27 update iOS modules