更新记录

1.0.1(2026-04-07)

  1. 增加切换扬声器/听筒接口
  2. 增加切换前后摄像头接口
  3. 增加开启/关闭视频流接口

1.0.0(2026-04-06)

  1. 视频通话

平台兼容性

uni-app x(3.7.3)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - 5.0 - -

其他

多语言 暗黑模式 宽屏模式

WebRTC音视频通话

主要功能

  1. 2人/多人音视频通话
  2. 静音/闭麦
  3. 切换摄像头
  4. 暂停/继续视频流
  5. 切换扬声器/听筒

uniapp版本https://ext.dcloud.net.cn/plugin?name=wrs-uts-webrtc

集成步骤

  • 拷贝demo里的Info.plist和AndroidManifest.xml到项目根目录
  • 咨询或定制请点击上面"进入交流群"按钮私聊作者
  • demo/static/NodeJS是websocket服务器,使用node app.js命令既可以运行,用户传送信令
  • 修改demo的socket服务器地址(webSocketUrl)改为电脑ip
  • socket业务可以使用各自的服务器,支持用来发送接收信令,可以使用socket、webSocket、socket.IO都可以,demo里的socket服务只是配合演示流程

socketTask = uni.connectSocket({
    url: 'ws://172.16.11.37:8088',
    complete: () => {}
});
  • 如果socket服务器是采用局域网IP连接,ios某些机型连接局域网时首次访问会弹出局域网授权信息,点击确定后再次点击界面上的"连接socket"按钮,socket连接状态可以查看控制台或代码
  • demo演示流程: 点击界面上的加入房间即可进行视频通话 整体业务流程:
  1. 点击"加入房间",会向房间里的其他人发送新成员加入消息

{
    msgType: "join",
    userId: "xx"
}
  1. 其他成员收到join消息时,会与该用户创建一个PeerConnection(同时把本地音视频加入PeerConnection),并生成offer,设置setLocalDescription,然后将offer数据发送给对方,同时会生成onIceCandidate,IceCandidate数据也要发送给对方

offer数据:

{
    msgType: "sdp",
    fromUserId: "xx",
    toUserId: "xx",
    type: "offer",
    sdp: "xx"
}

IceCandidate数据:
{
    msgType: "iceCandidate",
    fromUserId: “xx”,
    toUserId: "xx",
    id: candidate.sdpMid,
    label: candidate.sdpMLineIndex,
    candidate: candidate.sdp
}
  1. 用户收到offer消息时,也创建一个PeerConnection(同时把本地音视频加入PeerConnection),并setRemoteDescription,然后生成answer,设置setLocalDescription,然后将answer数据发送给对方,同时会生成onIceCandidate,IceCandidate数据也要发送给对方

answer数据:
{
    msgType: "sdp",
    fromUserId: "xx",
    toUserId: "",
    type: "answer",
    sdp: ""
}

IceCandidate数据:
{
    msgType: "iceCandidate",
    fromUserId: “xx”,
    toUserId: "xx",
    id: candidate.sdpMid,
    label: candidate.sdpMLineIndex,
    candidate: candidate.sdp
}
  1. 用户收到answer消息时,设置setRemoteDescription,双方收到对方的iceCandidate消息时,都调用addIceCandidate接口设置
  2. 完成以上流程后,就可以进行视频通话了

接口


import { UTSWebRTC } from "@/uni_modules/wrs-uts-webrtcx"

let webRTC = new UTSWebRTC()
  • 设置webRTC的回调

// 设置webRTC的回调
webRTC.onCallback((resp) => {
    let opt = resp.opt
    showMsg("webRTC.onCallback opt:" + opt)
    switch (opt) {
        // 信令状态改变
        case "onSignalingChange": {
            showMsg("onSignalingChange:" + JSON.stringify(resp))
            let state = resp.state
            if (state != null) {
                const stateValue = state! as number
                switch (stateValue) {
                    case 0: {
                        showMsg("RTCSignalingStateStable")
                    }
                        break;
                    case 1: {
                        showMsg("RTCSignalingStateHaveLocalOffer")
                    }
                        break;
                    case 2: {
                        showMsg("RTCSignalingStateHaveLocalPrAnswer")
                    }
                        break;
                    case 3: {
                        // 
                        showMsg("RTCSignalingStateHaveRemoteOffer")
                    }
                        break;
                    case 4: {
                        showMsg("RTCSignalingStateHaveRemotePrAnswer")
                    }
                        break;
                    case 5: {
                        showMsg("RTCSignalingStateClosed")
                        let userId = resp["userId"]
                        if (userId != null) {
                            userLeave(`${userId!}`)
                        }
                    }

                        break;
                    default:
                        break;
                }
            }
        }
            break;
        case "onIceGatheringChange": {
            showMsg("onIceGatheringChange:" + JSON.stringify(resp))
            let state = resp["state"]
            if (state != null) {
                let stateValue = state as number
                switch (stateValue) {
                    case 0: {
                        showMsg("RTCIceGatheringStateNew")
                    }
                        break;
                    case 1: {
                        showMsg("RTCIceGatheringStateGathering")
                    }
                        break;
                    case 2: {
                        showMsg("RTCIceGatheringStateComplete")
                    }
                        break;
                    default:
                        break;
                }
            }
        }
            break;
        // 生成IceCandidate
        case "onIceCandidate": {
             console.error(`onIceCandidate resp:${JSON.stringify(resp)}`)
            let toUserId = resp["userId"]
            let candidate = resp["iceCandidate"]
            if(candidate != null) {
                let candidateObj = candidate! as UTSJSONObject
                let sdpMid = candidateObj["sdpMid"]
                let sdpMLineIndex = candidateObj["sdpMLineIndex"]
                let sdp = candidateObj["sdp"]
                if (toUserId != null  && sdpMid != null && sdpMLineIndex != null && sdp != null) {
                    let params = {
                        msgType: "iceCandidate",
                        fromUserId: userId,
                        toUserId: toUserId!,
                        id: sdpMid!,
                        label: sdpMLineIndex!,
                        candidate: sdp!
                    }
                    showMsg(`发送 ice Candidate onIceCandidate:${JSON.stringify(params)}`)
                    sendSocketData(params)
                }
            }
        }
            break;
        case "onIceConnectionChange": {
            showMsg("onIceConnectionChange:" + JSON.stringify(resp))
            let state = resp.state
            switch (state) {
                case 0: {
                    showMsg("RTCIceConnectionStateNew")
                }
                    break;
                case 1: {
                    showMsg("RTCIceConnectionStateChecking")
                }
                    break;
                case 2: {
                    // 这步没有
                    showMsg("RTCIceConnectionStateConnected")
                }
                    break;
                case 3: {
                    showMsg("RTCIceConnectionStateCompleted")
                }
                    break;
                case 4: {
                    console.error("RTCIceConnectionStateFailed")
                    showMsg("RTCIceConnectionStateFailed")
                }
                    break;
                case 5: {
                    // 通讯被断开,一般是对方掉线或者STUN/TURN 服务器问题:如果 ICE 服务器配置不当,或者 STUN/TURN 服务器不可用,可能会导致连接失败。确保你的 STUN/TURN 服务器正常工作并且可达。
                    showMsg("RTCIceConnectionStateDisconnected")
                    let userId = resp.userId
                    if (userId != null) {
                        userLeave(`${userId!}`)
                    }
                }
                    break;
                case 6: {
                    showMsg(" RTCIceConnectionStateClosed")
                    let userId = resp.userId
                    if (userId != null) {
                        userLeave(`${userId!}`)
                    }
                }
                    break;
                case 7: {
                    showMsg(" RTCIceConnectionStateCount")
                }
                    break;
                default:
                    break;
            }

        }

            break;
        // 收到其他用户的音频或视频流
        case "onAddStream": {
            let tempUserId = resp["userId"]
            showMsg("onAddStream:" + JSON.stringify(resp))
            if (tempUserId != null) {
                let remoteUserId = `${tempUserId!}`
                let tempStream = resp["stream"]
                if (tempStream != null) {
                    // 如果有视频流,则显示其他用户的视频
                    let stream = tempStream! as UTSJSONObject
                    let tempVideoTracks = stream["videoTracks"]
                    if (tempVideoTracks != null) {
                        const videoTracks = tempVideoTracks! as Array<UTSJSONObject>
                        if (videoTracks.length > 0) {
                            let exist = existUser(remoteUserId)
                            if (!exist) {

                                 let paramsModel = {}
                                 paramsModel["userId"] = remoteUserId
                                 let businessModel = {}
                                 businessModel["business"] = "renderRemoteVideo"
                                 businessModel["params"] = paramsModel
                                 let businessArray = new Array<UTSJSONObject>()
                                 businessArray.push(businessModel)
                                 let renderParams = {}
                                 renderParams["businessArray"] = businessArray
                                let renderParamsStr = JSON.stringify(renderParams)

                                let userModel = new RemoteUser()
                                userModel.userId = remoteUserId
                                userModel.renderParams = renderParamsStr

                                console.log(`收到远程视频流:${JSON.stringify(userModel)} renderParams:${renderParamsStr}`)
                                otherPersons.value.push(userModel)
                            }
                        }
                    }
                }
            }

        }
            break;
        case "onRemoveStream": {

        }
            break;

        default:
            break;
    }
})
  • 初始化本地视频

// 初始化视频
webRTC.initVideoTrack({
    trackId: "video0",
    isScreencast: false // 仅对Android生效
})
  • 初始化本地音频

// 初始化音频
webRTC.initAudioTrack({
    trackId: "audio0"
})
  • 配置音频,仅支持iOS

webRTC.configureAudioSession({
    category: "playAndRecord",
    mode: "voiceChat"
})
  • 开启相机抓流/切换摄像头

// 开始本地抓流
let params = {
    isFront: this.isFront,
    width: 1280, // width仅支持Android
    height: 720, // height仅支持Android
    fps: 30
}
// params.cameraName = "xxx" //摄像头名称,传了cameraName的话isFront无效,cameraName从webRTC.getAllCameras()接口获取
webRTC.startVideoCapture(params)
  • 获取所有摄像头名称, 仅支持Android

let result = webRTC.getAllCameras()
let cameraArray = result.cameras
  • 暂停抓流

webRTC.stopVideoCapture()
  • 开启/关闭本地视频

let enable = true
webRTC.setLocalVideoTrackEnable(enable)
  • 开启/关闭本地音频

let enable = true
webRTC.setLocalAudioTrackEnable(enable)
  • 创建连接

iceServers支持类型: 打洞服务器,可以自己搭建https://github.com/coturn/coturn

  1. 第一种

{
   urls: ["xxx"]
}
  1. 第二种

{
   urls: ["xxx"],
   username: "xx", // 账号
   credential: "xx" // 密码
}

let iceServers = [{
    urls: ["stun:stun.l.google.com:19302",
        "stun:stun1.l.google.com:19302",
        "stun:stun2.l.google.com:19302",
        "stun:stun3.l.google.com:19302",
        "stun:stun4.l.google.com:19302"
    ],
    username: "xxxx",
    credential: "xxxx"
}]
let params = {} 
params["userId"] = userId
// params.iceServers = iceServers
// params.sdpSemantics = 1 // 0: RTCSdpSemanticsPlanB 1:RTCSdpSemanticsUnifiedPlan
// params.continualGatheringPolicy =
//  1 // 0: RTCContinualGatheringPolicyGatherOnce 1: RTCContinualGatheringPolicyGatherContinually
// params.constraints = {
//  mandatory: {},
//  optional: {
//      DtlsSrtpKeyAgreement: "true"
//  }
// }
userId = webRTC.createPeerConnection(params)
  • 将本地视频加入连接

let videoResp = webRTC.addVideoTrack({
    userId: userId,
    streamIds: ["video0"]
})
let videoFlag = videoResp.flag
if (!videoFlag) {
    this.showMsg("添加本地视频出错:" + JSON.stringify(videoFlag))
}
  • 将本地音频加入连接

let audioResp = webRTC.addAudioTrack({
    userId: userId,
    streamIds: ["audio0"]
})
let audioFlag = audioResp["flag"]! as boolean
if (!audioFlag) {
    this.showMsg("添加本地音频出错:" + JSON.stringify(videoFlag))
}
  • 创建offer

webRTC.createOffer({
    userId: userId,
    setLocalDescription: false
}, (resp) => {
    let flag = resp["flag"]! as boolean
    if (flag) {
        let sessionDescription = resp["sessionDescription"]
        let type = sessionDescription["type"]
        let sdp = sessionDescription["sdp"]
        }
    }
)
  • 设置本地LocalDescription

webRTC.setLocalDescription({
    userId: userId,
    type: type, // 支持offer、pranswer、answer
    sdp: sdp
}, (localDescResp) => {
    let localFlag = localDescResp["flag"]! as boolean
    if (localFlag) {
    }
    }
)
  • 设置远程RemoteDescription

webRTC.setRemoteDescription({
    userId: userId,
    type: type, // 支持offer、pranswer、answer
    sdp: sdp
}, (resp) => {
    let flag = resp["flag"]! as boolean
    if (flag) {
        }
    }
)
  • 创建answer

webRTC.createAnswer({
    userId: userId,
    setLocalDescription: false
}, (answerResp) => {
    let flag = answerResp["flag"]! as boolean
    if (flag) {
        // console.log("createAnswer result:" + JSON.stringify())
        let sessionDescription = answerResp["sessionDescription"]
        let type = sessionDescription["type"]
        let sdp = sessionDescription["sdp"]
        }
    }
)
  • 添加候选人IceCandidate,一般调用offer或answer时会生成多次IceCandidate,可以都发送给对方,对方设置多次

webRTC.addIceCandidate({
    userId: userId,
    sdpMid: sdpMid,
    sdpMLineIndex: sdpMLineIndex,
    sdp: sdp
}, (resp) => {
    let flag = resp.flag
    if (!flag) {
        this.showMsg("addIceCandidate error:" + JSON.stringify(resp))
    }
})
  • 销毁某个用户的连接

webRTC.destroyPeerConnection({
    userId: userId
})
  • 销毁所有的连接

webRTC.destroyAllPeerConnection()

UI组件

使用wrs-uts-webrtc-view组件的页面要用nvue


<wrs-uts-webrtcx :style="'width:'+width+'px;height:'+height+'px;'" :params="localViewParams"
    @onEvent="onLoadLocalView"></wrs-uts-webrtcx>
  • 渲染本地视频 通过修改localViewParams参数来实现业务

// 渲染本地视频界面
let businessArray = new Array<UTSJSONObject>()
businessArray.push({
    business: "renderLocalVideo" // 业务名称,渲染本地画面
})
let params = {}
params["businessArray"] = businessArray
localViewParams.value = JSON.stringify(params)
  • 渲染其他用户视频

let paramsModel = {}
 paramsModel["userId"] = remoteUserId // 用户ID
 let businessModel = {}
 businessModel["business"] = "renderRemoteVideo" // 渲染远程用户画面
 businessModel["params"] = paramsModel
 let businessArray = new Array<UTSJSONObject>()
 businessArray.push(businessModel)
 let renderParams = {}
 renderParams["businessArray"] = businessArray
let renderParamsStr = JSON.stringify(renderParams)
  • 切换前后摄像头

webRTC.startVideoCapture({
    isFront: true, // 是否使用前摄像头
    width: 1280, // 仅对Android生效
    height: 720 // 仅对Android生效
})
  • 开启/关闭视频流

webRTC.setLocalVideoTrackEnable(true)
  • 切换扬声器/听筒

webRTC.setSpeakerEnable(true)

隐私、权限声明

1. 本插件需要申请的系统权限列表:

1. 相机、麦克风

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率: