下载或者安装 sip.js 到 uniapp 项目,APP 端在 menifest.json 中配置麦克风权限
menifest.json 中 app 权限配置选中:
android.permission.RECORD_AUDIO
android.permission.MODIFY_AUDIO_SETTINGS
sip.js 低版本 如 V0.13.0 版本的写法
注册 取消注册 呼叫 {{isInOut?inComingNumber:outGoingNumber}} 通话中... 呼入... 呼出... import * as sip from "@/common/js/sip-0.13.0.min.js" let ua; export default { name: "VoiceIntercom", props: {}, data() { return { configuration: {}, outGoingNumber: '02700002', baseUrl: 'web.domain.com', // sipurl格式: "sip:02700001@web.domain.com:7065", port: '7065', user: { number: '02700001', name: 'test', password: '123456' }, server: 'wss://web.domain.com:7067', currentSession: null, inComingNumber: null, isRegistered: false, isConnected: false, // 是否接通 isInOut: true, // true 被呼, false 呼出 } }, onLoad() { this.handleRegister() }, beforeDestroy() { this.handleUnRegister() }, methods: { // 登录 handleRegister() { this.configuration = { uri: `sip:${this.user.number}@${this.baseUrl}:${this.port}`, displayName: this.user.name, password: this.user.password, transportOptions: { wsServers: [this.server], traceSip: true }, } ua = new sip.UA(this.configuration) ua.on('registered', (resp) => { this.showTishi('【' + this.user.number + '】语音登录成功') this.isRegistered = true }) ua.on('registrationFailed', (resp) => { if (resp.statusCode == 503) { this.showTishi('【' + this.user.number + '】服务不可用') } else { this.showTishi('【' + this.user.number + '】语音登录失败:' + resp.reasonPhrase) } this.isRegistered = false console.log(resp, '语音登录失败') }) ua.on('unregistered', (response, cause) => { this.showTishi('【' + this.user.number + '】取消语音登录成功') console.log(response, cause, '取消语音登录') }) ua.on('invite', (session) => { this.currentSession = session this.inComingNumber = session.remoteIdentity.uri.user this.isInOut = true this.$refs.popup.open() this.$nextTick(() => { this.$refs.bell.$refs.audio.play() this.$refs.bell.$refs.audio.currentTime = 0 }) this.sessionEvent(session) }) ua.on('message', (message)=>{ console.log(message,'ua-message') }) ua.start() }, // 退出登录 handleUnRegister() { if (ua) { ua.unregister() this.isRegistered = false } }, // session 处理 sessionEvent(session) { session.on('rejected', () => { console.log('inComing挂断') }) session.on('cancel', () => { console.log('outgoing挂断') }) session.on('terminated', (message, cause) => { console.log(message, cause, 'session-terminated') if (cause == 'Rejected') { if (message.reasonPhrase == 'Decline') { this.showTishi('您的拨号暂时无人接听!') } else { this.showTishi('对方拒接了!') } } else if (cause == 'BYE') { this.showTishi('对方已挂机!') } else if (cause == 'Canceled') { this.showTishi('对方已取消!') } this.$refs.bell.$refs.audio.pause() this.$refs.popup.close() }) session.on('accepted', (resp) => { this.$refs.bell.$refs.audio.pause() this.isConnected = true console.log(resp, '接受了') }) session.on('trackAdded', () => { const pc = session.sessionDescriptionHandler.peerConnection const remoteStream = new MediaStream() pc.getReceivers().forEach((receiver) => { if (receiver.track) { remoteStream.addTrack(receiver.track) this.$refs.remoteAudio.$refs.audio.srcObject = remoteStream this.$refs.remoteAudio.$refs.audio.play() } }) }) session.on('bye', (resp, cause) => { console.log(resp, cause, 'session-bye') if ((resp && resp.method == 'BYE') || cause == 'BYE') { this.isConnected = false this.$refs.popup.close() this.$refs.remoteAudio.$refs.audio.pause() this.showTishi('【' + this.user.number + '】通话已结束!') } }) session.on('failed', () => { console.log('session-failed') }) }, // 接听 handleAccept() { const option = { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } } this.currentSession.accept(option) }, // 拒接 handleTerminate() { this.currentSession.terminate() this.$refs.popup.close() this.isConnected = false }, // 挂断 handleCacel() { if (this.isInOut) { this.currentSession.reject() } else { this.currentSession.terminate() } this.$refs.popup.close() this.isConnected = false }, // 拨打 handleCall(number) { number = this.outGoingNumber if (this.isRegistered) { this.isInOut = false const sipUrl = `sip:${number}@${this.baseUrl}:${this.port}` this.currentSession = ua.invite(sipUrl, { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }) this.$refs.popup.open() this.sessionEvent(this.currentSession) } else { this.showTishi('请先登录语音用户') } }, showTishi(title){ uni.showToast({ title: title, icon: 'none' }) }, } } .container { font-size: 30rpx; } .top-box { padding: 30rpx; display: flex; } .popup-box { background: #ffffff; width: 80vw; padding: 40rpx; border-radius: 20rpx; line-height: 80rpx; } .flex-box { display: flex; justify-content: space-around; .uni-tag { font-size: 30rpx; padding: 16rpx 28rpx; } } .fs-36{ font-size: 36rpx; font-weight: bold; }
sip.js 高版本如 V0.21.2 用法(参数同上,只列出 methods 里的部分)
import { UserAgentOptions, UserAgent, Registerer, Invitation, Inviter, Session, SessionState, InvitationAcceptOptions, InviterOptions, Messager, URI, RegistererState, } from '@/common/js/sip-0.21.2.min.js' let userAgent, registerer; const target = UserAgent.makeURI(`sip:${this.outGoingNumber}@${this.baseUrl}:${this.port}`); methods: { handleRegister() { const uri = UserAgent.makeURI(`sip:${this.user.number}@${this.baseUrl}:${this.port}`) if(!uri){ console.log('创建URI失败') } const transportOptions = { server: this.server } const userAgentOptions = { authorizationUsername: this.user.number, authorizationPassword: this.user.password, displayName: this.user.name, transportOptions, uri, delegate: { onInvite } } userAgent = new UserAgent(userAgentOptions) registerer = new Registerer(userAgent) userAgent.start().then(()=>{ registerer.register({ requestDelegate: { onReject: (resp)=>{ console.log(resp, 'onReject') }, onAccept: (resp)=>{ console.log(resp, 'onAccept') }, onProgress: (resp) => { console.log(resp, 'onProgress') }, onRedirect: (resp) => { console.log(resp, 'onRedirect') }, onTrying: (resp) => { console.log(resp, 'onTrying') }, } }).catch((e: Error) => { console.log(e, "Register failed") }) }).catch((err:any)=>{ console.log(err,'start-err') }) registerer.stateChange.addListener((newState)=>{ switch (newState) { case RegistererState.Unregistered: console.log('退出登录') break; case RegistererState.Registered : break; case RegistererState.Initial : console.log('语音用户登录Initial') break; case RegistererState.Terminated : console.log('语音用户登录Terminated') break; } }) function onInvite(invitation){ this.currentSession = invitation this.inComingNumber = invitation.remoteIdentity.uri.user this.$refs.popup.open() invitation.stateChange.addListener((state)=>{ this.sessionStateEvent(state, invitation) }) } } handleAccept(){ let constrainsDefault = { audio: true, video: false, } const options = { sessionDescriptionHandlerOptions: { constraints: constrainsDefault } } this.currentSession.accept(options) this.isConnected = true } handleReject(){ this.currentSession.reject() this.$refs.popup.close() } sessionStateEvent(state, session){ switch(state){ case SessionState.Initial: console.log('SessionState.Initial') case SessionState.Establishing: console.log('SessionState.Establishing') break; case SessionState.Established: console.log('SessionState.Established') this.isConnected = true this.setupRemoteMedia(session) break; case SessionState.Terminating: console.log('SessionState.Terminating') break; case SessionState.Terminated: console.log('SessionState.Terminated') this.clearupMedia(session) break; } } setupRemoteMedia(session){ const remoteStream = new MediaStream() // console.log(session.sessionDescriptionHandler, 'sessionDescriptionHandler') session.sessionDescriptionHandler.peerConnection.getReceiver().forEach((receiver)=>{ if(receiver.track){ remoteStream.addTrack(receiver.track) } }) this.$refs.remoteAudio.$refs.audio.srcObject = remoteStream this.$refs.remoteAudio.$refs.audio.play() } clearupMedia(session){ if(this.isCallIn){ if(session.isCanceled){ console.log('对方已挂机') } }else{ if(!session.isCanceled){ // console.log('对方已挂机') } } this.$refs.remoteAudio.$refs.audio.srcObject = null this.$refs.remoteAudio.$refs.audio.pause() this.endCall() } handleCall(){ this.isCallIn = false this.$refs.popup.open() const inviterOptions = { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } } if(target){ const inviter = new Inviter(userAgent, target, inviterOptions) this.currentSession = inviter inviter.invite({ requestDelegate: { onReject: (resp)=>{ console.log(resp, 'inviter-onReject') if(resp.statusCode == 500){ console.log('对方不在线') }else{ console.log('对方拒接了') } }, onAccept: (resp)=>{ console.log(resp, 'inviter-onAccept') }, onProgress: (resp) => { console.log(resp, 'inviter-onProgress') }, onRedirect: (resp) => { console.log(resp, 'inviter-onRedirect') }, onTrying: (resp) => { console.log(resp, 'inviter-onTrying') }, } }) inviter.stateChange.addListener((state)=>{ this.sessionStateEvent(state, inviter) }) } } handleURegister(){ if(userAgent){ this.isRegistered = false registerer.unregister() } } sendMessage(){ if(target && this.isRegistered){ const messager = new Messager(userAgent, target, '你好') messager.message() } } endCall(){ this.isConnected = false this.$refs.popup.close() switch(this.currentSession.state){ case SessionState.Initial: case SessionState.Establishing: if(this.currentSession instanceof Inviter){ // incoming session this.currentSession.cancel() }else{ // outgoing session this.currentSession.reject() } break; case SessionState.Established: this.currentSession.bye() break; case SessionState.Terminating: break; case SessionState.Terminated: console.log(SessionState,'Terminated-endCall') break; } } }
APP模式下检测麦克风权限,permission.js可从插件市场直接下载,使用页面import引入。
getPermission(){ let env = uni.getSystemInfoSync().platform if (env === 'android') {permission.requestAndroidPermission('android.permission.RECORD_AUDIO').then((e) => { if (e === -1) { uni.showToast({ title: '您已经拒绝录音权限,请在应用设置中手动打开', icon: 'none', }) } else if (e === 0) { uni.showToast({ title: '您拒绝了录音授权', icon: 'none', }) } else if (e === 1) { console.log('已授权') } else { uni.showToast({ title: '授权返回值错误', icon: 'none', }) } }).catch((err) => { uni.showToast({ title: '拉起录音授权失败', icon: 'none', }) }) } else if (env === 'ios') { if (permission.judgeIosPermission("record")){ console.log('已授权') }else{ uni.showToast({ title: '您拒绝了录音授权,请在应用设置中手动打开', icon: 'none', }) } } }