天翼云实时云渲染 JS SDK
ecloud-rcr2
toc
安装
npm install ecloud-rcr2
一、快速启动
1.1 前端项目引入
import { RCRLaunch } from 'ecloud-rcr2';
const bootstrap = async () => {
try {
const hostElement = document.getElementById("container");
const launch = await RCRLaunch({
appKey,
hostElement,
uiOptions: {
onChange: (cb) => {
const { phase, fakePercent } = cb;
console.log('百分⽐:',fakePercent);
// launcherBase 实例获取是在 phase ⾄少为 'signaling-connected' 阶段,即
if (phase === "signaling-connected") {
console.log("信令已连接")
// const connection = launch.launcherBase.connection;
}
},
onPhaseChange: (phase, deltaTime) => {
if (phase === "signaling-connected") {
const { connection, player } = launch.launcherBase;
console.log(connection, player);
}
},
onPlay: () => {
console.log('出现了有效画⾯');
},
}
});
} catch (error) {
console.error(error);
}
};
bootstrap();
1.2 浏览器直接引入
// 注意全局对象的名称
const { RCRLaunch } = window.ECloudRCR;
二、启动器(Launcher)
import { RCRLaunch } from 'ecloud-rcr2';
const launch = RCRLaunch({
appKey,
hostElement,
baseOptions: { ... },
uiOptions: { ... },
});
2.1 Launcher 构造属性(Constructors)
属性 | 说明 | 类型 | 是否必填 |
---|
appKey | 应用 key ,优先从 url 中获取同名 query | string | - |
hostElement | 云渲染容器,默认使用 #container DOM 元素 | HTMLElement | - |
baseOptions | 基础配置 | BaseOptionsType | - |
uiOptions | UI 配置 | UIOptions | - |
2.2 baseOptions 基础配置
属性 | 说明 | 类型 | 是否必填 |
---|
startType | 1:普通连接 3:投屏连接,默认1 | number | - |
appSecret | 密钥 | string | - |
exeParameter | 自定义应用启动参数 | string | - |
webrtcEnable | 是否开启webrtc推流,默认为true(详见示例) | boolean | - |
isCastScreenMaster | (投屏)是否为主控 | boolean | - |
nickname | (投屏-观看端)昵称 | string | - |
2.3 UIOptions UI配置
默认值 "-" 代表该配置优先从云端服务(前台应⽤配置)获取,或者是平台默认特性
2.3.1 渲染配置
属性 | 说明 | 类型 | 是否必填 | 默认值 |
---|
loadingImage | 加载过程 loading 图 | String|HTMLImageElement | - | - |
loadingBgImage | 加载过程背景图(区分横竖屏) | { portrait: String, landscape: String } | - | - |
loadingBarImage | 加载过程⼩图标(设置背景图时候⽣效) | String|HTMLImageElement | - | - |
showDefaultLoading | 未设置 loading 时,是否需要显示默认 loading 图(如有) | boolean | 否 | False |
showFakePercent | 展示加载百分⽐ | boolean | 否 | True |
phaseTextMap | 初始化加载⽂案配置 | Map<Phase, number,string> | 否 |
minBitrate | 云渲染最⼩码率(kbs)(优先级⾼) | number | - | 2000 |
maxBitrate | 云渲染最⼤码率(kbs)(优先级⾼) | number | - | 5000 |
startBitrate | 云渲染初始码率(kbs)(优先级⾼) | number | - | 4000 |
rateLevel | 云渲染码率等级(优先级低) | RateLevel | 否 | 1 |
landscapeType | 显示模式 | LandscapeType | 否 | - |
needLandscape | 开启强制横屏(不再⾃动旋转流⽅向以适应容器⼤⼩) | boolean | 否 | - |
autoLoadingVideo | 是否⾃动挂载云渲染⾳视频(移动端适用) | boolean | 否 | True |
audioToastDisplay | 是否开启音频提示弹窗 | boolean | - | True |
// 云渲染码率等级(优先级低)
enum RateLevel {
SD, // 0 流畅
HD, // 1 ⾼清
FHD, // 2 超清
UHD4K, // 3 蓝光
}
// 显示模式
enum LandscapeType {
Auto = 1, // 1 ⾃适应模式(默认)
Auto, // 2 拉伸模式,IOS不⽀持
Auto, // 3 裁剪模式
}
2.3.2 输入控制
属性 | 说明 | 类型 | 是否必填 | 默认值 |
---|
settingHoverButton | ⼯具栏显示情况 | VirtualControlDisplayType | 否 | - |
toolbarLogo | ⼯具栏图标 | string | 否 | - |
toolOption | 工具栏选项 | ToolOptionType | - | |
openMicrophone | 开启⻨克⻛输⼊ | boolean | 否 | - |
openMultiTouch | 开启应⽤多点触控 | boolean | 否 | False |
eventOption | 事件选项(多用于自定义键盘事件) | EventOptionType | - | |
keyboardMappingConfig | 键⿏映射配置 | VirtualGlobalType | 否 | - |
inputHoverButton | PC 输⼊框显示情况 | InputHoverButton | 否 | - |
disablePointerManager | 是否禁⽤指针样式同步(常见于游戏应用改变节点机鼠标样式,默认同步显示) | boolean | 否 | False |
disablePointerLock | 是否禁⽤指针锁定(默认开启指针锁定,应用会进入相对移动模式,鼠标不会移出页面范围) | boolean | 否 | False |
注:
- v2.x sdk 开启多点触控时,不再需要对接插件
- v1.x sdk 开启多点触控是,需要接入 3DCAT 的 UE4 插件,并在后台管理->应用详情->设置->开启多点触控。
// ⼯具栏显示情况
enum VirtualControlDisplayType {
HideAll, // 全平台隐藏
DisplayMobile, // 移动端可⻅
DisplayPc, // PC端可⻅
DisplayAll, // 全平台可⻅
}
// 工具栏所属平台
enum ToolsPlatformType {
Pc = 1, // pc 1
Mobile, // 移动端 2
All, // 全部 3
}
// 工具栏选项
interface ToolOptionType {
dropTools?: DropToolsType; // 删除⼯具栏
extendTools?: ExtendToolsType[]; // 新增⼯具栏
}
interface DropToolsType {
toolsIndex: number[]; // ⼯具栏位置(从 0 开始)
platform: ToolsPlatformType;
}
interface ExtendToolsType {
icon: string; // 图标(base64/url)
text: string; // 标题
platform: ToolsPlatformType;
order: number; //新增位置(从 0 开始)
onClick: () => void; //点击触发事件
}
// 事件选项
interface EventOptionType {
enableKeyBoard?: boolean; // 是否挂载键盘事件(默认为 true)
}
// 键⿏映射配置
interface VirtualGlobalType {
resolutionRatio: string // 推流分辨率 如 `1920*1080`
buttonSize: ButtonSizeType // 按键⼤⼩
perspectiveShift: PerspectiveShiftType // 遗留参数,作废
showButton: boolean// 是否显示键⿏映射控件
showAlias: boolean // 是否显示按键别名
landscape: VirtualDataType[]
portrait: VirtualDataType[]
}
type VirtualDataType = {
type: ScreenDataType//控件类型
value: SolutionValue//控件数据
scale?: number//⽐例
}
// PC 输⼊框显示情况
enum InputHoverButton {
Hide, // 不可⻅
Display, // 可⻅
}
2.3.3 回调监听
属性 | 说明 | 类型 | 是否必填 | 默认值 |
---|
percentChanged | 是否监听云渲染加载百分⽐变化 | boolean | 否 | True |
phaseChanged | 是否监听云渲染阶段变化 | boolean | 否 | True |
onChange | 监听云渲染阶段/加载百分⽐变化(任一变化都触发) | (cb: OnChange) => void | 否 | |
onPhaseChange | 监听云渲染阶段变化 | (phase: Phase, deltaTime: number) => void | 否 | - |
onRunningId | 监听云渲染 runningId 返回 | (runningId: number) => void | 否 | |
onRunningOptions | 监听云渲染连接服务参数 | (opt: OnRunningOptions) => void | 否 | |
onRotate | (视图)视频容器方向变化回调 | (rotate: boolean) => void | - | |
onQueue | 监听排队变化 | (rank: number) => void | 否 | |
onPlay | 监听云渲染展示有效画面 | () => void | - | |
onRtmpActive | (数据)监听是否有datachannel消息返回 | (result: boolean) => void | 否- | |
onGetTicketCallBack | (异常)监听校验错误信息 | (res: GetTicketRes) => void | 否- | |
onLoadingError | (异常)监听加载过程报错(包含渲染错误) | (err: LoadingError) => void | 否 | |
onError | (异常)监听云渲染报错 | (reason: ErrorState) => void | 否 | - |
onMount | (工具栏)监听云渲染虚拟控件挂载节点 | (el: HTMLElement) => void | - | |
onQuit | (工具栏)监听主动关闭(⼯具栏关闭按钮) | () => void | 否 | - |
onShowUserList | (投屏)监听是否展示投屏列表 | (showCastScreenUsers: boolean) => void | 否 | |
onSupportTransfer | (投屏)监听是否⽀持转移控制权 | (supportTransfer: boolean) => void | 否 | |
onLiveState | (直播)监听设置直播回调 | (liveState: LiveStateType) => void | 否 | |
// 百分⽐进度-状态-数值说明
const PhasePercentMap = new Map<Phase, [number, string]>([
["initial", [0, "可视化服务启动中..."]],
["signaling-connected", [25, "可视化服务连接中..."]], // 0-25
["node-ready", [45, "可视化服务连接中..."]], // 25-45
["streaming-ready", [55, "可视化服务连接中..."]],
["end-candidate", [65, "可视化服务连接中..."]],
["peer-connection-connected", [85, "可视化服务连接中..."]],
["data-channel-open", [90, "连接成功,资源加载中..."]],
["loaded-metadata", [99, "连接成功,资源加载中..."]],
["streaming-playing", [100, "连接成功,资源加载中..."]],
]);
// 云渲染-阶段说明
type Phase =
| 'initial' // 初始化
| 'signaling-connected' // 信令已连接
| 'node-ready' // 节点就绪
| 'end-candidate' // webRTC 候选结束
| 'peer-connection-connected' // RTCPeerConnection 已连接
| 'data-channel-open' // RTCDataChannel 已连接
| 'streaming-ready' // 视频流刚开始接收(⿊屏⽆画⾯)
| 'loaded-metadata' // 视频流 loadedmetadata 事件触发
| 'streaming-playing' // 视频流 play(有画⾯)
// 监听云渲染阶段/加载百分⽐变化
interface OnChange {
phase: Phase; // 渲染阶段
fakePercent: number; // 连接百分⽐(参考PhasePercentMap)
deltaTime: number; // 耗时
}
// 监听云渲染连接服务参数
interface OnRunningOptions {
token: string; // 信令token
signaling: string; // 信令服务器
coturns: RTCIceServer[]; // ICE 服务器地址信息 https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
}
/**
* 云渲染报错
* disconnect: 应⽤连接已断开
* afk: 应⽤⻓时间未进⾏操作,已⾃动结束运⾏
* kick: 当前应⽤已在其他⻚⾯打开(开启重连配置)
* hangup: 当前应⽤已在其他⻚⾯打开(开启重连配置)
*/
type ErrorState = 'disconnect' | 'afk' | 'kick' | 'hangup';
// 监听加载过程报错
interface LoadingError {
code: number | string;
/**
* app: app启动阶段报错
* task: app调度阶段报错
* connection: app已连接阶段报错(reason:ErrorState)
*/
type: 'app' | 'task' | 'connection';
reason: string | ErrorState;
}
// 监听校验错误信息
interface GetTicketRes {
codeImage: string; // 验证码base64
errorNum: number; // 密钥验证失败次数
temporaryCredentials: string; // 凭证
}
// 投屏连接
interface ClientsType {
isMaster: boolean; // 是否为主控
nickname: string; // ⽤户名
isControlAuthority: boolean; // 是否有操控权
token: string;
}
2.3.4 更多能力
属性 | 说明 | 类型 | 是否必填 | 默认值 |
---|
enableLogPersistent | 允许持久化记录云渲染⽇志 | boolean | 否 | True |
enableTCP | 设置ICE以tcp⽅式连接,默认仅支持udp | boolean | 否 | False |
iceTransportPolicy | ICE 传输策略,all|relay | RTCIceTransportPolicy | 否 | 'all' |
autorunRivatuner | 启动统计信息,可供前端下载webrtc-stat分析数据 | boolean | 否 | False |
disableFileTransfer | 是否拒绝接收远端应用推送⽂件给 web | boolean | 否 | False |
2.4 Launcher 实例属性(Attributes)
属性名 | 说明 | 类型 |
---|
loading | Loading 实例,详见三 | LoadingCompoent |
launcherBase | LauncherBase 实例,详见四 | LauncherBase |
2.5 Launcher 实例方法(Methods)
方法名 | 说明 | 参数 | 回调参数 |
---|
destory | 删除sdk渲染实例 | (text: String, opt:{ videoScreenshot: Boolean }) | void |
liveStart | (直播)设置直播流url地址,并开始推送;传参为空时,使用之前的url地址 | string | void |
liveStop | (直播)停⽌推直播流数据 | - | void |
liveUrl | (直播)设置直播url地址,url地址必填 | (url?: string,delay?:number//超时时间,默认为 20s) | void |
launcher.destory("关闭应⽤");
//关闭之后并保持最后⼀帧视频流作为背景图
launcher.destory("关闭应⽤", { videoScreenshot: true });
三、Loading 实例
Demo: Loading 实例
3.1 属性(Attributes)
属性名 | 说明 | 类型 | 默认值 |
---|
loadingCompoent | loading实例组件 | Loading | - |
3.2 方法(Methods)
方法名 | 说明 | 类型 | 返回值 |
---|
showLoadingText | 修改loading⽂本以及控制百分⽐(进度)显示 | (text: string, showPercent?: boolean) | void |
changePhase | 主动改变phase状态,loading⽂本以及百分⽐会被动变化 | Phase | void |
destroy | 注销loading组件 | | void |
四、LauncherBase 实例
const launch = await RCRLaunch({
...
uiOptions: {
onChange: (cb) => {
const { phase, fakePercent } = cb;
// launcherBase 实例获取是在 phase ⾄少为 'signaling-connected' 阶段,即
if (phase === "signaling-connected") {
// const { connection, player } = launch.launcherBase;
}
},
onPlay: () => {
console.log(launch.launcherBase);
}
}
});
4.1 属性(Attributes)
属性名 | 说明 | 类型 | 默认值 |
---|
player | LivePlayer实例 | LivePlayer | LivePlayer |
connection | Connection实例 | Connection | Connection |
moveSensitivity | 获取⿏标或者触摸移动的敏感度 | TouchMoveTypeEnum | TouchMoveTypeEnum |
// ⿏标或者触摸移动的敏感度
enum TouchMoveTypeEnum {
NORMAL, // 正常
HIGH, // ⾼
HIGHMAX, // 最⾼
}
4.2 方法(Methods)
方法名 | 说明 | 参数 | 返回值 |
---|
report | 获取实时连接状态 | | Object |
setMoveSensitivity | 设置⿏标或者触摸移动的敏感度 | (type:TouchMoveTypeEnum) | void |
resumeVideoStream | ⼿动触发播放 | | void |
toggleStatistics | 切换统计数据开关(默认关) | | void |
handleSubscribe | ⼿动挂载⿏标、键盘、触控等事件 | (ele:HTMLElement) | void |
handleUnsubscribe | ⼿动取消⿏标、键盘、触控等事件 | | void |
toggleFullscreen | 切换全屏(横屏),⽀持情况 | | void |
openMicrophone | 开启⻨克⻛(后台可设置初始化开启⻨克⻛) | - | |
closeMicrophone | 关闭⻨克⻛ | | void |
toggleVirtualControl | 切换键⿏映射显示状态(默认显示,与键⿏映射设置显示按钮优先级⼀致) | | void |
setLogAccordingToLevel | 设置统计数据级别; 1:log/info,2:warn,3:error | (lavel:number) | |
showDashboard | 显示仪表盘推流性能数据 | | void |
hideDashboard | 隐藏仪表盘推流性能数据 | | void |
exportLog | 导出webrtc-internals统计数据 | | void |
downloadFileByAbsolutePath | 下载远端节点⽂件(绝对路径+⽂件名) | (path: string) | |
destory | 断开全部连接 | | void |
handleChangeSubscribe | (投屏)切换猫头⼯具的控制权 | (status:boolean) | void |
五、Connection 实例
const launch = await RCRLaunch({
...
uiOptions: {
onPlay: () => {
const { connection } = launch.launcherBase;
}
}
});
5.1 属性(Attributes)
5.2 方法(Methods)
方法名 | 说明 | 参数 | 回调参数 |
---|
emitUIInteraction | 发送消息到应用程序 | string | ArrayBuffer | Promise\<boolean> |
send | 发送消息到应⽤程序(第⼆个参数为true可重置超时时间) | string|Blob|ArrayBuffer|boolean | void |
changeBandwidthByRenegotiation | 调节码率 | number | void |
changeEncodeResolution | 修改云端推流的编码分辨率(默认1920*1080) | {width?: number; height?: number; } | void |
screenshot | 截图云端截图 | {index?: number; left?: number; top?: number; width?: number; height?: number;} | void |
controlAuthority | 投屏转移控制权限(详细参考投屏示例) | token | void |
5.3 事件(Events)
事件名 | 说明 | 回调参数 |
---|
connect | 可视化服务连接中 | - |
dataChannelConnected | 连接成功,资源加载中 | - |
close | 连接中断回调 | CloseEvent |
disconnect | 信令断开回调 | string |
interaction | 接收应用端返回数据 | string |
afk | 超时断开 | - |
screenshot | 完整接收完截图数据时;payload 为截图数据 | Blob |
screenshotData | 收到截图数据时;payload 为数据字节⻓度 | number |
六、Player 实例
const launch = await RCRLaunch({
...
uiOptions: {
onPlay: () => {
const { player } = launch.launcherBase;
}
}
});
6.1 属性(Attributes)
6.2 方法(Methods)
方法名 | 说明 | 参数 | 回调参数 |
---|
setVideoVolume | 设置 video 播放⾳量值(应⽤声⾳,ios设备不⽀持) | number | void |
handleChangeLandscapeType | 设置显示模式 | number(1:自适应|2:拉伸|3:裁剪) | void |
handleChangeOrientationLock | 移动端调节旋转 | boolean(false:横屏|true:竖屏) | void |
七、鼠标键盘控制相关接口(可选扩展)
当您需要对鼠标、键盘进行定制时(eg:组合键、改键映射),可以使用下面的能力。
7.1 键盘事件(Keyboard)
import { RCRLaunch, Keyboard } from 'ecloud-rcr2';
let eventConnection;
const launch = await RCRLaunch({
...
uiOptions: {
uiOptions: {
enableKeyBoard: false, // 关闭默认键盘挂载
},
onPlay() {
// 处理键盘映射
},
onPhaseChange: (phase, deltaTime) => {
if (phase === "signaling-connected") {
const { connection } = launch.launcherBase;
eventConnection = onnection
}
}
},
});
// eg:发送键盘事件
document.addEventListener('keyup', (e) => {
e.preventDefault()
eventConnection.send(Keyboard.fromKeyboardEvent(e, false).dumps(), true)
})
参数说明:
属性 | 说明 | 类型 |
---|
keycode | 键码 | number |
alt | 按下 alt | boolean |
shift | 按下 shift | boolean |
ctrl | 按下 ctrl | boolean |
nlock | 按下 nlock | boolean |
clock | 按下 clock | boolean |
slock | 按下 slock | boolean |
down | 按下 | boolean |
7.2 鼠标移动事件(MouseMove)
import { MouseMove } from 'ecloud-rcr2';
connection.send(new MouseMove(100, 300, 3, 4).dumps());
参数说明:
属性 | 说明 | 类型 |
---|
x | x | number |
y | y | number |
dx | dx | number |
dy | dy | number |
7.3 鼠标按钮事件(MouseButton)
import { MouseButton } from 'ecloud-rcr2';
connection.send(new MouseButton(1, true).dumps());
参数说明:
属性 | 说明 | 类型 |
---|
mouseButtonType | left = 1,middle=2, right=3 | number |
down | 按下 | boolean |
7.4 缩放事件(WheelScroll)
import { WheelScroll } from 'ecloud-rcr2';
connection.send(new WheelScroll(10, true).dumps());
参数说明:
属性 | 说明 | 类型 |
---|
step | step | number |
forward | forward | boolean |
7.5 触控事件(TouchSet)
import { TouchSet } from 'ecloud-rcr2';
connection.send(new TouchSet(0, [{ x: 10, y: 10, id: 1 }]).dumps());
参数说明:
属性 | 说明 | 类型 |
---|
touchType | down = 0,update=1, up=2 | number |
touchList | TouchPoint:{x: number y: number id: number} //id:touch identifier | TouchPoint[] |
7.6 文本传输事件(TextInput)
import { TextInput } from 'ecloud-rcr2';
connection.send(new TextInput('test text').dumps());
参数说明:
属性 | 说明 | 类型 |
---|
text | 聚焦应用输入框时能够快速发送内容,比如实现复制粘贴功能 | string |
八、常用问题
8.1 接入微信小程序
- 步骤
1. 使用 jssdk 开发,打包成 html 部署到线上(生产环境需要域名解析)
2. 微信小程序通过
web-view
嵌入 html 地址(域名链接) - 开发 html 时候,微信(小程序)环境需要注意监听
WeixinJSBridgeReady
事件,然后开始对接 jssdk
window.addEventListener('DOMContentLoaded', () => {
if (navigator.userAgent.includes('miniProgram') || navigator.userAgent.includes('MicroMessenger')) {
//微信浏览器/微信小程序环境
document.addEventListener('WeixinJSBridgeReady', bootstrap, false);
} else {
bootstrap();
}
});
8.2 WebRTC 调试参考
- 方法1:在任一浏览器标签地址打开
chrome://webrtc-internals
,即可看到 WebRTC 的完整调试数据 - 方法2:调⽤
Launcher showDashboard
⽅法,显示仪表盘推流性能数据(注:空数据情况下记得调⽤ launcher toggleStatistics
⽅法切换状态) - 方法3:调⽤
Launcher exportLog
⽅法,导出 webrtc-internal s统计数据(注:空数据情况下记得调⽤ launcher toggleStatistics
⽅法切换状态)
8.4 投屏分享功能
功能说明:
- 投屏交互分为主控端、观看端
- 主控端需要使用投屏 appKey 初始化应用
- 主控端生成观看端链接
- 主控端可以赋予/回收临时操控权给观看端(需要创建投屏链接时勾选该功能)
主控端示例:
let clientToken;
const launch = await RCRLaunch({
appKey, // 投屏专用 appKey ,需要先增加投屏链接获取
address, // 投屏需要指定集群地址,和观看端保持同集群
baseOptions: {
startType: 3, // 投屏链接
isCastScreenMaster: true,
},
uiOptions: {
onMount: () => {
// 获取投屏观看端临时 appKey ,拼接投屏地址
// 投屏地址可以直接使用当前页面(定义 query 参数来区分端),或使用独立的分享页面来进行物理分隔
copyUrl = `${window.location.origin}${window.location.pathname}?appKey=${key}&type=share`;
},
onRunningOptions: (options) => {
clientToken = options.token;
},
onPhaseChange: (phase, deltaTime) => {
if (phase === "signaling-connected") {
const { connection } = launch.launcherBase;
connection.event.clientInfo.on((data) => {
renderClients(data);
});
}
}
toolOption: {
// 生成投屏的功能按钮
extendTools:[
{
icon: "your icon",
text: "访问邀请",
platform: 3,
onClick: () => {
// 复制生成的投屏地址
},
},
],
},
},
});
function renderClients(data) {
const list = data.clients || [];
list.forEach((client) => {
const { nickname, isControlAuthority, token } = client;
// 渲染投屏用户列表,注意:
// 1、仅主控端可以进行权限转移
// 2、列表样式用户自行定义
// 判断当前用户是自己
if (token === clientToken) {
launch.launcherBase.handleChangeSubscribe(isControlAuthority); // 切换猫头工具的控制权
isControlAuthority ? launch.launcherBase.handleSubscribe(videoEle) : launch.launcherBase.handleUnsubscribe(); // 订阅交互 or 取消订阅交互
}
});
};
观看端示例:
const bootstrap = async (nickname) => {
try {
const launch = await RCRLaunch({
// appKey, // 不需要填,默认从 query 中读取观看端 appKey
address, // 需要显式指定集群地址,确保和主控端在同一集群
baseOptions: {
startType: 3, // 投屏链接
isCastScreenMaster: false,
nickname: nickname, // 分享端必填
},
uiOptions: {
onPhaseChange: (phase, deltaTime) => {
if (phase === "signaling-connected") {
const { connection } = launch.launcherBase;
connection.event.clientInfo.on((data) => {
//
});
}
}
}
});
} catch (error) {
console.error(error);
}
};
// 分享端要先拿到分享信息,渲染一个需要输入 nickname 的界面给用户填写,填写后再启动应用