差别

这里会显示出您选择的修订版和当前版本之间的差别。

到此差别页面的链接

im:web:draft:multiuserconference [2019/03/11 06:55] (当前版本)
jk 创建
行 1: 行 1:
 +====== 多人实时通话 ======
  
 +----
 +
 +===== 产品简介 =====
 +
 + ​为满足不同场景需求,[[https://​github.com/​easemob/​webim/​tree/​master/​webrtc/​dist/​EMedia_x1v1.js|EMedia_x1v1.js]]不仅提供了对1v1通话的支持,而且提供了多人通话功能。多人实时音视频会议划分了不同的类型,不同类型对应了不同场景,使你能够轻松地将实时音视频功能集成到你的应用或者网站中。本片将介绍多人音视频使用接入。可参照[[https://​github.com/​easemob/​webim|demo]]。
 + ​在创建会议时可以传入以下几种类型:
 +
 +      - Communication:普通通信会议,最多支持参会者6人,会议里的每个参会者都可以自由说话和发布视频,该会议类型在服务器不做语音的再编码,音质最好,适用于远程医疗,在线客服等场景;
 +      - Large Communication:大型通信会议,最多参会者30人,会议里的每个参会者都可以自由说话,最多支持6个人发布视频,该会议模式在服务器做混音处理,支持更多的人说话,适用于大型会议等场景;
 +      - Live:互动视频会议,会议里支持最多6个主播和600个观众,观众可以通过连麦与主播互动,该会议类型适用于在线教育,互动直播等场景。
 +
 +
 +==== 产品特性 ====
 +
 +    *支持现代浏览器:Chrome/​50+、Safari/​11+、Firefox;
 +
 +    *遵守:UMD通用模块规范,支持require导入;
 +
 +    *支持Promise;
 +
 +    *支持手机端和 Web 端互通,极大方便开发者的全平台业务;
 +
 +    *依赖https站点
 +
 +==== 音视频通信的简要步骤 ====
 +
 +SDK 能够支持音频和视频通信。创建音视频通信的过程简单来说,可以分为以下几步:
 +
 +    1. 初始化 SDK,设置监听代理
 +    2. create: 创建会议
 +    3. join: 加入会议
 +    4. pub: 发布音视频数据流
 +    5. sub: 订阅并播放音视频数据流
 +    6. leave: 离开会议
 +==== 基本知识 ====
 +
 +emedia.mgr.ConfrType:多人会议类型
 +    1. Communication:普通通信会议,最多支持参会者6人,成员都可以自由说话和发布视频,成员角色Speaker
 +    2. Large Communication:大型通信会议,最多参会者30人,成员都可以自由说话和发布视频,成员角色Speaker
 +    3. Live:互动视频会议,会议里支持最多6个主播和600个观众
 +<code javascript>​
 +emedia.mgr.ConfrType = {
 +   ​COMMUNICATION:​ 10, //​普通会议模式
 +   ​COMMUNICATION_MIX:​ 11, //​大会议模式
 +   LIVE: 12, //​直播模式
 +};
 +</​code>​
 +<code javascript>​
 +emedia.mgr.Role = {
 +    ADMIN: 7, // 能创建会议,销毁会议,移除会议成员,切换其他成员的角色,订阅流,发布流
 +    TALKER: 3, // 能上传自己的音视频,能观看收听其他主播的音视频,即能发布流和订阅流)
 +    AUDIENCE: 1 // 观众Audience:只能观看收听音视频,即只能订阅流
 +};
 +</​code>​
 +<code javascript>​
 +//​conference|confr
 +{
 +    confrId:"​TS_X296786295944036352C27",​
 +    id:"​TS_X296786295944036352C27",​
 +    password: "​password123", ​
 +    roleToken:"​roleToken",​
 +    ticket:"​ticket",​
 +    type:12
 +};
 +</​code>​
 +<code javascript>​
 +//member
 +{
 +    "​ext":​{ //​emedia.mgr.joinConference(confrId,​ password, {role: '​admin'​})/​* 用户可自定义扩展字段*/​);​
 +        "​role":"​admin"​
 +    },
 +    "​id":"​MS_X197721744293023744C19M197756407719972865VISITOR",​
 +    "​globalName":"​easemob-demo#​chatdemoui_yss000@easemob.com",​
 +    "​name":​ "​yss000"​
 +}
 +</​code>​
 +<code javascript>​
 +//stream
 +{
 +    "​id":"​RTC2__Of_C19M197756407719972865VISITOR",​
 +    "​voff":​0,​ //1 视频关闭 ​
 +    "​aoff":​0,​ //1 音频关闭 ​
 +    "​memId":"​MS_X197721744293023744C19M197756407719972865VISITOR",​
 +    "​owner":​ member ,//​member对象
 +    "​rtcId":"​RTC1"​
 +}
 +var islocated = stream.located();​ //islocated true 本地媒体流
 +</​code>​
 +    ​
 +    注意:
 +    >> 每个人必须调用join接口成功后,才算是加入会议(即成为会议成员)。会议成员才允许进行其他操作比如订阅流、发布流等
 +    >> 成员如果想改变自己角色,必须想办法通知管理员,只有管理员才能修改
 +
 +===== 多人通信会议功能实现 =====
 +
 +如何使用SDK实现多人实时音视频会议
 + 
 +Communication和Large Communication除了最大成员数不一样,流程几乎是一样的。以下是从创建会议到离开会议完整的流程讲解:
 +
 +==== 设置SDK回调 ====
 +
 +进入会议之前,设置SDK回调后,可获知成员加入或离开会议,数据流更新等。
 +
 +<code javascript>​
 +//​有人加入会议,其他人调用joinXX等方法,如果加入成功,已经在会议中的人将会收到
 +emedia.mgr.onMemberJoined = function (member) {
 +    ​
 +};
 +</​code>​
 +<code javascript>​
 +//​有人退出会议
 +emedia.mgr.onMemberExited = function (member) {
 +    ​
 +};
 +</​code>​
 +<code javascript>​
 +//​有媒体流添加;比如 自己调用了publish方法(stream.located() === true时),或其他人调用了publish方法。
 +emedia.mgr.onStreamAdded = function (member, stream) {
 +
 +};
 +</​code>​
 +<code objc>
 +//​有媒体流移除
 +emedia.mgr.onStreamRemoved = function (member, stream) {
 +
 +};
 +</​code>​
 +<code javascript>​
 +//​角色改变
 +emedia.mgr.onRoleChanged = function (role) {
 +
 +};
 +</​code>​
 +<code javascript>​
 +//​会议退出;自己主动退 或 服务端主动关闭;
 +emedia.mgr.onConferenceExit = function (reason, failed) {
 +    reason = (reason || 0);
 +    switch (reason){
 +        case 0:
 +            reason = "​正常挂断";​
 +            break;
 +        case 1:
 +            reason = "​没响应";​
 +            break;
 +        case 2:
 +            reason = "​服务器拒绝";​
 +            break;
 +        case 3:
 +            reason = "​对方忙";​
 +            break;
 +        case 4:
 +            reason = "​失败,​可能是网络或服务器拒绝";​
 +            if(failed === -9527){
 +                reason = "​失败,​网络原因";​
 +            }
 +            if(failed === -500){
 +                reason = "​Ticket失效";​
 +            }
 +            if(failed === -502){
 +                reason = "​Ticket过期";​
 +            }
 +            if(failed === -504){
 +                reason = "​链接已失效";​
 +            }
 +            if(failed === -508){
 +                reason = "​会议无效";​
 +            }
 +            if(failed === -510){
 +                reason = "​服务端限制";​
 +            }
 +            break;
 +        case 5:
 +            reason = "​不支持";​
 +            break;
 +        case 10:
 +            reason = "​其他设备登录";​
 +            break;
 +        case 11:
 +            reason = "​会议关闭";​
 +            break;
 +    }
 +};
 +</​code>​
 +
 +
 +==== 用户A创建会议 ====
 +
 +<code javascript>​
 +emedia.mgr.createConference(confrType,​ password).then(function(confr){
 +}).catch(function(error){
 +})
 +</​code>​
 +
 +    注意: 如果A只单纯的create,没进行join操作,则A不是Conference的成员,没有相应的角色,不能进行其他操作
 +    ​
 +==== 用户A进入会议 ====
 +<code javascript>​
 +emedia.mgr.joinConferenceWithTicket(confrId,​ ticket, ext).then(function(confr){
 +
 +}).catch(function(error){
 +
 +})
 +</​code>​
 +==== 管理员A邀请其他人加入会议 ====
 +
 +SDK没有提供邀请接口,你可以自己实现,比如使用环信IM通过发消息邀请,比如通过发邮件邀请等等。
 +
 +至于需要发送哪些邀请信息,可以参照SDK中的join接口,目前是需要Conference的confrId和password
 +
 +比如用环信IM发消息邀请
 +<code javascript>​
 +WebIM.call.inviteConference(confrId,​ password, jid, gid)
 +</​code>​
 +
 +    注意:使用环信IM邀请多个人时,建议使用群组消息。如果使用单聊发消息请注意每条消息中间的时间间隔,以防触发环信的垃圾消息防御机制
 +==== 用户B接收到邀请加入会议 ====
 +
 +
 +用户B解析出邀请消息中带的confrId和password,调用SDK的join接口加入会议,成为会议成员且角色是Speaker.
 +
 +<code javascript>​
 +//​javascript
 +emedia.mgr.joinConference(confr,​ password, ext).then(function(confr){
 +
 +}).catch(function(error){
 +
 +})
 +</​code>​
 +
 +
 +
 +用户B成功加入会议后,会议中其他成员会收到回调[emedia.mgr.onMemberJoined(memberB)]
 +
 +==== 成员A发布音视频流 ====
 +
 +
 +成员A和成员B都有发布流的权限
 +
 +<code javascript>​
 +/**
 + * constaints: {audio: true, video: true}
 + * videoTag 可缺失,如果有 此次publish的媒体数据将会在这个video上显示 将会与stream绑定
 + * ext 用户自定义扩展,其他成员可以看到这个字段
 + ​* ​
 + */
 +emedia.mgr.publish(constaints,​ videoTag, ext).then(function(pushedStream){
 +    //stream 对象
 +}).catch(function(error){
 +
 +});
 +
 +/**
 + * videoTag 可缺失时
 + *
 + */
 +emedia.mgr.publish(constaints,​ ext).then(function(pushedStream){
 +    //stream 对象
 +    //​如果需要将这个stream对象 显示,需要 emedia.mgr.streamBindVideo(videoTag,​ pushedStream)
 +}).catch(function(error){
 +
 +});
 +</​code>​
 +  *** 注意:A推流成功后,onStreamAdded 将被回调 ***
 +==== 其他成员收到通知并订阅流 ==== 
 +
 +成员A成功发布数据流后,会议中其他成员会收到监听类回调[emedia.mgr.onStreamAdded],如果成员B想看成员A的音视频,可以调用subscribe接口进行订阅
 +
 +<code javascript>​
 +//​其他成员
 +emedia.mgr.onStreamAdded = function (member, stream) {
 +   ​if(!stream.located()){ //​stream.located() === true, 是自己发布刚刚发布的流
 +      emedia.mgr.subscribe(member,​ stream, true, true, video)
 +   }
 +};
 +</​code>​
 +
 +
 +==== 成员B取消订阅流 ====
 +
 +成员B如果不想再看成员A的音视频,可以调用SDK接口unsubscribe
 +
 +<code javascript>​
 +//​emedia.mgr.hungup(stream);​
 +emedia.mgr.unsubscribe(stream);​
 +</​code>​
 +
 +==== 成员A取消发布流 ====
 +
 +成员A可以调用unpublish接口取消自己已经发布的数据流,操作成功后,会议中收到回调[emedia.mgr.onStreamRemoved:​removeStream:​] ,将对应的数据流信息移除
 +
 +<code javascript>​
 +//​emedia.mgr.hungup(pushedStream);​
 +emedia.mgr.unpublish(pushedStream);​
 +</​code>​
 +
 +<code javascript>​
 +emedia.mgr.onStreamRemoved = function (member, stream) {
 +   ​if(stream.located(){ //​自己发布流
 +   ​}else{ //​会议其他人发布的流
 +   }
 +};
 +</​code>​
 +
 +==== 成员B离开会议 ====
 +
 +成员B调用SDK接口exitConference离开会议,会议中的其他成员会收到回调[emedia.mgr.onMemberExited:​member:​]
 +
 +<code javascript>​
 +emedia.mgr.exitConference();​
 +</​code>​
 +
 +
 +===== 互动视频会议功能实现 =====
 +
 +互动视频会议的基本操作(创建、邀请人、发布流、取消发布流、订阅流、取消订阅流、更新发布流程、离开)对应的接口和回调同通信会议是一样的。也可以说 互动视频会议是在通信会议的基础上,增加了角色管理功能,以下着重讲解互动视频会议中的角色管理相关知识点
 + 
 + 1. 创建互动视频会议时,接口emedia.mgr.createConference传入的type参数是emedia.mgr.ConfrType.LIVE
 + 
 + 2. 创建者createAndJoin后的角色是Admin,其他成员第一次调用接口[emedia.mgr.joinConference(confrId,​ password, ext)]加入直播后的权限是观众Audience,Audience只能订阅数据流
 + 
 + 3. 观众Audience如果想发布数据流 即上麦,需要给管理员发申请。SDK没有提供申请接口,你可以自定义。
 + 
 +管理员如果同意Audience上麦,需要调用接口emedia.mgr.grantRole将角色Audience更改为Speaker
 + 
 +<code javascript>​
 +emedia.mgr.grantRole(confr,​ [memberName1,​ memberName2],​ emedia.mgr.Role.TALKER)
 +</​code> ​
 +     
 +成员角色改变后,被改变的成员会收到回调
 +
 +<code javascript>​
 +emedia.mgr.onRoleChanged = function (role) {
 +
 +};
 +</​code> ​
 + 
 +4. 角色从Audience变为Speaker,成员就可以发布数据流了
 +
 +===== 其他接口 =====
 +==== 获取加入会议ticket ====
 +<code javascript>​
 +emedia.mgr.getConferenceTkt(confrType,​ password).then(function(confr){
 +    ​
 +}).catch(function(error){
 +
 +})
 +</​code> ​
 +
 +==== 销毁会议 ====
 +<code javascript>​
 +emedia.mgr.destroyConference(confr).then(function(){
 +    ​
 +}).catch(function(error){
 +
 +})
 +</​code> ​
 +==== 踢人 ====
 +<code javascript>​
 +emedia.mgr.kickMembersById(confr,​ memberNames).then(function(){
 +    ​
 +}).catch(function(error){
 +
 +})
 +</​code> ​
 +==== 授权 ====
 +<code javascript>​
 +emedia.mgr.grantRole(confr,​ memberNames,​ role).then(function(){
 +    ​
 +}).catch(function(error){
 +
 +})
 +</​code> ​
 +
 +==== 使用用户名密码加入会议,可自定义ext ====
 +<code javascript>​
 +emedia.mgr.joinConference(confrId,​ password, ext).then(function(){
 +    ​
 +}).catch(function(error){
 +
 +})
 +</​code> ​
 +
 +==== 使用ticket加入会议,可自定义ext ====
 +<code javascript>​
 +emedia.mgr.joinConference(confrId,​ ticket, ext).then(function(){
 +    ​
 +}).catch(function(error){
 +
 +})
 +</​code> ​
 +
 +==== 退出会议 ====
 +<code javascript>​
 +emedia.mgr.exitConference();​
 +</​code>​
 +
 +==== publish 媒体流 ====
 +<code javascript>​
 +/**
 + * constaints: {audio: true, video: true}
 + * videoTag 可缺失,如果有 此次publish的媒体数据将会在这个video上显示 将会与stream绑定
 + * ext 用户自定义扩展,其他成员可以看到这个字段
 + ​* ​
 + */
 +emedia.mgr.publish(constaints,​ videoTag, ext).then(function(pushedStream){
 +    //stream 对象
 +}).catch(function(error){
 +
 +});
 +
 +/**
 + * videoTag 可缺失时
 + *
 + */
 +emedia.mgr.publish(constaints,​ ext).then(function(pushedStream){
 +    //stream 对象
 +    //​如果需要将这个stream对象 显示,需要 emedia.mgr.streamBindVideo(videoTag,​ pushedStream)
 +}).catch(function(error){
 +
 +});
 +</​code>​
 +
 +==== 共享桌面,​仅支持PC Chrome或electron平台 ====
 +<code javascript>​
 +/**
 + * videoConstaints {screenOptions:​ ['​screen',​ '​window',​ '​tab'​]} or true
 + * withAudio: true 携带语音,false不携带
 + * videoTag 可缺失,如果有 此次publish的媒体数据将会在这个video上显示 将会与stream绑定
 + * ext 用户自定义扩展,其他成员可以看到这个字段
 + ​* ​
 + */
 +emedia.mgr.shareDesktopWithAudio(videoConstaints,​ withAudio, videoTag, ext).then(function(pushedStream){
 +    //stream 对象
 +}).catch(function(error){
 +
 +});
 +
 +//​electron平台 默认选择第一个屏幕,如果需要选择其他,需要重写方法
 +emedia.chooseElectronDesktopMedia = function(sources,​ accessApproved){
 +    var firstSources = sources[0];
 +    accessApproved(firstSources);​
 +}
 +</​code> ​
 +
 +==== 取消publish ====
 +<code javascript>​
 +emedia.mgr.unpublish(pushedStream);​
 +
 +emedia.mgr.hungup(pushedStream);​
 +</​code> ​
 +
 +==== 订阅 ====
 +<code javascript>​
 +/**
 + * subSVideo 看视频
 + * subSAudio 看音频
 + * videoTag 可缺失,如果有 将会与stream绑定,并播放stream
 + */
 +emedia.mgr.subscribe(member,​ stream, subSVideo, subSAudio, videoTag).then(function(){
 +    //stream 对象
 +}).catch(function(error){
 +
 +});
 +emedia.mgr.triggerSubscribe(videoTag,​ subSVideo, subSAudio).then(function(){
 +    //stream 对象
 +}).catch(function(error){
 +
 +});
 +也可以
 +//​仅订阅音频
 +emedia.mgr.triggerResumeAudio(videoTag).then(function(){
 +}).catch(function(error){
 +});
 +//​仅暂停订阅音频
 +emedia.mgr.triggerPauseAudio(videoTag).then(function(){
 +}).catch(function(error){
 +});
 +
 +//​仅订阅视频
 +emedia.mgr.triggerResumeVideo(videoTag).then(function(){
 +}).catch(function(error){
 +});
 +//​仅暂停订阅视频
 +emedia.mgr.triggerPauseVideo(videoTag).then(function(){
 +}).catch(function(error){
 +});
 +
 +</​code>​
 +
 +==== stream video 绑定 ====
 +<code javascript>​
 +//stream 与 video绑定后,video才可以播放stream;并且可以通过触发/​监听video上的事件,来达到对stream的便捷操作
 +emedia.mgr.streamBindVideo(stream,​ videoTag);
 +</​code>​
 +
 +==== 获取stream绑定的video ====
 +<code javascript>​
 +emedia.mgr.getBindVideoBy(stream);​
 +</​code>​
 +
 +==== 切换摄像头====
 +<code javascript>​
 +emedia.mgr.switchCamera().then(function(){
 +
 +}).catch(function(){
 +
 +})
 +</​code>​
 +
 +==== 暂停/​恢复自己的视频 ====
 +<code javascript>​
 +emedia.mgr.pauseVideo(pubS).then(function(){
 +
 +}).catch(function(){
 +
 +})
 +等价于
 +emedia.mgr.triggerResumeVideo(localVideoTag).then(function(){
 +
 +}).catch(function(){
 +
 +})
 +</​code>​
 +<code javascript>​
 +emedia.mgr.pauseVideo(pubS).then(function(){
 +
 +}).catch(function(){
 +
 +})
 +等价于
 +emedia.mgr.triggerPauseVideo(localVideoTag).then(function(){
 +
 +}).catch(function(){
 +
 +})
 +</​code>​
 +
 +==== 抓取 video图像,并保存 ====
 +<code javascript>​
 +emedia.mgr.captureVideo(videoTag,​ true, filename)
 +等价于
 +emedia.mgr.triggerCaptureVideo(videoTag,​ true, filename);
 +</​code>​
 +
 +==== 控制远程视频(手机端)定格 ====
 +<code javascript>​
 +emedia.mgr.freezeFrameRemote(stream);​
 +等价于
 +emedia.mgr.triggerFreezeFrameRemote(videoTag).catch(function(){
 +    alert("​定格失败"​);​
 +});
 +</​code>​
 +
 +==== 控制手机闪光灯打开/​关闭 ====
 +<code javascript>​
 +/**
 + * torch true 打开,否则 关闭; 可缺失
 + */
 +emedia.mgr.torchRemote(stream, torch);
 +等价于
 +emedia.mgr.triggerTorchRemote(videoTag,​ torch).catch(function(){
 +    alert("​Torch失败"​);​
 +});
 +</​code>​
 +
 +==== 控制手机截屏 ====
 +<code javascript>​
 +emedia.mgr.capturePictureRemote(stream);​
 +等价于
 +emedia.mgr.triggerCapturePictureRemote(videoTag).catch(function(){
 +    alert("​抓图失败"​);​
 +});
 +</​code>​
 +
 +==== 控制手机摄像头放大缩小 ====
 +<code javascript>​
 +emedia.mgr.zoomRemote(stream,​ multiples);
 +等价于
 +emedia.mgr.triggerZoomRemote(videoTag,​ multiples).catch(function(){
 +    alert("​zoom失败"​);​
 +});
 +</​code>​
 +
 +==== 控制手机摄像头聚焦曝光 ====
 +<code javascript>​
 +/**
 + * clickEvent 为 videoTag的点击事件。通过event计算点击的坐标传给sdk进行控制
 + *
 + */
 +emedia.mgr.focusExpoRemote(stream,​ videoTag, clickEvent).catch(function(){
 +    alert("​focusExpoRemote失败"​);​
 +});
 +等价于
 +/**
 + * event string. 如点击 “click”
 + * fail 失败回调;success成功回调
 + */
 +emedia.mgr.onFocusExpoRemoteWhenClickVideo(videoTag,​ event, fail, success);
 +</​code>​
 +
 +==== 取消在videoTag上的事件 ====
 +<code javascript>​
 +//​用来对onFocusExpoRemoteWhenClickVideo的撤销
 +emedia.mgr.offEventAtTag(videoTag);​
 +</​code>​
 +
 +==== video监听的事件 ====
 +<code javascript>​
 +//​video的音频视频开关,将会触发
 +emedia.mgr.onMediaChanaged(videoTag,​ function (constaints) {
 +    $div.find("#​aoff"​).html(constaints.audio ? "​有声"​ : "​无声"​);​
 +    $div.find("#​voff"​).html(constaints.video ? "​有像"​ : "​无像"​);​
 +
 +    //​可以通过video标签 得知发布方 是否关闭 音频 视频
 +    console.warn($(videoTag).attr("​easemob_stream"​),​ "​aoff",​ $(videoTag).attr("​aoff"​));​
 +    console.warn($(videoTag).attr("​easemob_stream"​),​ "​voff",​ $(videoTag).attr("​voff"​));​
 +});
 +</​code>​
 +
 +<code javascript>​
 +//​video的声音大小出发,比如谁在说话
 +emedia.mgr.onSoundChanaged(videoTag,,​ function (meterData) {
 +    var $volume = $div.find('#​volume'​);​
 +
 +    $volume.html(meterData.instant.toFixed(2)
 +        + " " + meterData.slow.toFixed(2)
 +        + " " + meterData.clip.toFixed(2)
 +        + " " + (meterData.trackAudioLevel ? parseFloat(meterData.trackAudioLevel).toFixed(4) : "​--"​)
 +        + " " + (meterData.trackTotalAudioEnergy ? parseFloat(meterData.trackAudioLevel).toFixed(4) : "​--"​)
 +    );
 +});
 +</​code>​
 +
 +<code javascript>​
 +//​视频收发数据统计
 +emedia.mgr.onMediaTransmission(videoTag,​ function notify(trackId,​ type, subtype, data) {
 +    var $iceStatsShow = $div.find("#​iceStatsShow"​);​
 +    var $em = $iceStatsShow.find("#"​+subtype);​
 +    if(!$em.length){
 +        $em  = $("<​em></​em>"​).appendTo($iceStatsShow).attr("​id",​ subtype);
 +    }
 +
 +    $em.text(subtype + ":"​ + (data*8/​1000).toFixed(2));​
 +});
 +</​code>​
 +
 +<code javascript>​
 +//​连接状态变化
 +emedia.mgr.onIceStateChanged(videoTag,​ function (state) {
 +    console.log(state);​
 +});
 +</​code>​
 +
 +
 +----
 +<WRAP group>
 +<WRAP half column>
 +上一页:[[im:​web:​basics:​videocall|实时通话]]
 +</​WRAP>​
 +
 +<WRAP half column>
 +下一页:[[im:​web:​other:​toolrelated|工具类说明]]
 +</​WRAP>​
 +</​WRAP>​