FreeSWITCH 简单图形化界面38 - 在uniapp中使用JsSIP进行音视频呼叫
- 0、测试环境
- 1、学习uniapp
- 2、测试代码
- main.js
- utils/render.js
- store/data.js
- pages/index/index.vue
- pages.json
- 3、效果
- 4、难点
0、测试环境
http://myfs.f3322.net:8020/
用户名:admin,密码:admin
FreeSWITCH界面安装参考:https://blog.csdn.net/jia198810/article/details/137820796
1、学习uniapp
在学习JsSIP的时候,之前写过几个 demo ,并试图将其拓展到手机端应用。采用纯 Web 页面在手机浏览器环境下,借助 WSS 协议能够顺利达成通信效果。但考虑到实际的使用场景,需要将其封装为独立的 APP 更好。
JsSIP 的音视频功能必须依赖 WSS 协议才能实现,否则浏览器会限制音频或视频设备的调用,并且需要有效的证书支持。即便证书不受浏览器信任,用户还可以手动选择 “信任该证书,继续访问” 来维持功能的正常使用。
鉴于 Uniapp 本质上也是基于网页技术,之前认为在 Uniapp 中同样需要可信任的证书,尤其是对于自签名证书而言,由于 APP 中不存在 “信任该证书,继续访问” 这样的手动操作选项,就放弃了在 Uniapp 上的测试。
后来,看下了Uniapp 框架的教程,经过测试,发现 Uniapp 不仅能够成功运行 JsSIP 库,而且在使用 WSS 协议时,证书似乎被默认信任了(我不知道为什么,但实际效果是可以正常使用)。
值得注意的是,Uniapp 本身并不直接具备调用 JsSIP 的能力,但通过其提供的 renderjs ,可以实现调用JsSIP库。调用JsSIP后,基本就是复制之前的demo代码。
2、测试代码
基本流程就是index.vue界面变量发生变化后–>触发render.js里的jssip逻辑。
就写了一个页面,代码结构:
main.js
// #ifndef VUE3
import Vue from 'vue'
import { reactive } from 'vue'
import App from './App'
Vue.config.productionTip = falseApp.mpType = 'app'const app = new Vue({...App,
})
app.$mount()
// #endif//用了个pinia
//#ifdef VUE3
import { createSSRApp } from 'vue'
import App from './App.vue'
import { reactive } from 'vue'
import * as Pinia from 'pinia';export function createApp() {const app = createSSRApp(App)app.use(Pinia.createPinia());return {app,Pinia}
}
// #endif
utils/render.js
import JsSIP from "jssip";
import {toRaw,inject
} from "vue";export default {data() {return {// 是否有音频设备hasAudioDevice: false,// 初始化 uaMyCaller: null,// 当前会话currentCall: null,//当前呼叫类型currentCallMediaType: null,// 连接服务器失败次数connectCount: 5,ua: null,//分机设置setting: {username: "1020",password: "1020",wssServer: "210.51.10.231",wssPort: 7443,userAgent: "MyWssPhone",},//当前分机状态status: {isRegistered: false,isUnregistered: false,isConnecting: false,isDisconnected: false,isNewMessage: false,isIncomingCall: false,isOutgoingCall: false,isIceCandidate: false,isProgress: false,isAccepted: false,},//jssip的socketsocket: null,//被叫号码callee: "",//呼叫放行originator: "",//呼叫媒体mediaType: "audio",//音频控件ringtone: null,ringtoneSound: "./static/sounds/ringin.wav",//本地视频控件localVideoElement: null,//本地视频父控件localVideoElementId: "local-video",//本地视频控件媒体流localMediaStream: null,//播放状态localVideoIsPlaying: false,//远程视频控件remoteVideoElement: null,//远程视频父控件remoteVideoElementId: "remote-video",//远端媒体流remoteMediaStream: null,//日志enableLog: true,log: ""};},mounted() {},methods: {// 日志showLog(...data) {this.log = data.join("");if (this.enableLog) {//JsSIP.debug.enable('JsSIP:*');console.log(this.log);}},//接收vue页面username值updateUsername(newValue, oldValue) {this.showLog("用户名变化:", newValue, oldValue);this.setting.username = newValue;},// 接收vue页面password值updatePassword(newValue, oldValue) {this.showLog("密码变化:", newValue, oldValue);this.setting.password = newValue;},// 接收vue页面wssServer值updateWssServer(newValue, oldValue) {this.showLog("服务器地址变化:", newValue, oldValue);this.setting.wssServer = newValue;},// 接收vue页面wssPort值updateWssPort(newValue, oldValue) {this.showLog("Wss端口变化:", newValue, oldValue);this.setting.wssPort = newValue;},// 接收vue页面callee值updateCallee(newValue, oldValue) {this.showLog("被叫号码变化:", newValue, oldValue);this.callee = newValue;},// 停止一切handleStop(newValue, oldValue) {if (!newValue) {return}//停止if (this.ua) {this.showLog("注销并停止ua");this.ua.unregister()this.ua.stop();}if (this.socket) {this.socket.disconnect()this.showLog("断开连接");}},//监听vue页面,login 数据变化,处理注册handleRegister(newValue, oldValue) {if (!newValue) {this.showLog("注销或者不注册")this.handleStop()return}if (!this.setting.username || !this.setting.password || !this.setting.wssServer || !this.setting.wssPort) {this.showLog("数据为空");return;}const jssip_uri = new JsSIP.URI("sip",this.setting.username,this.setting.wssServer,this.setting.wssPort);this.showLog("uri", jssip_uri);const wss_uri = `wss://${this.setting.wssServer}:${this.setting.wssPort}`;this.socket = new JsSIP.WebSocketInterface(wss_uri);const configuration = {sockets: [this.socket],uri: jssip_uri.toAor(),authorization_user: this.setting.username,display_name: this.setting.username,register: true,password: this.setting.password,realm: this.setting.wssServer,register_expires: 300,user_agent: this.setting.userAgent,contact_uri: `sip:${this.setting.username}@${this.setting.wssServer};transport=wss`};JsSIP.C.SESSION_EXPIRES = 180, JsSIP.C.MIN_SESSION_EXPIRES = 180;this.showLog("配置参数", configuration);if (this.ua) {this.ua.stop();}this.ua = new JsSIP.UA(configuration);this.ua.on("registrationFailed", (e) => {this.onRegistrationFailed(e);});this.ua.on('registered', (e) => {this.onRegistered(e);});this.ua.on('unregistered', (e) => {this.onUnregistered(e);});this.ua.on("connecting", (e) => {this.onConnecting(e);});this.ua.on("disconnected", (response, cause) => {this.onDisconnected(response, cause);});this.ua.on("newMessage", (e) => {this.onNewMessage(e);});this.ua.on('newRTCSession', (e) => {this.showLog("新呼叫:", e)this.showLog("当前呼叫:主叫号码", e.request.from.uri.user);this.showLog("当前呼叫:被叫号码", e.request.to.uri.user);this.currentCall = e.session;this.originator = e.originator;if (this.originator === "remote") {//来电this.onInComingCall(e);} else {//去电this.onOutGoingCall(e);}this.currentCall.on("connecting", (e) => {this.showLog("呼叫连接中")});this.currentCall.on("icecandidate", (e) => {this.onIceCandidate(e);});this.currentCall.on("progress", (e) => {this.onProgress(e);});this.currentCall.on("accepted", (e) => {this.onAccepted(e);});this.currentCall.on("peerconnection", (e) => {this.onPeerConnection(e);});this.currentCall.on("confirmed", (e) => {this.onConfirmed(e);});this.currentCall.on("sdp", (e) => {this.onSDP(e);});this.currentCall.on("getusermediafailed", (e) => {this.onGetUserMediaFailed(e);});this.currentCall.on("ended", (e) => {this.onEnded(e);});this.currentCall.on("failed", (e) => {this.onFailed(e);});});this.ua.start();},// 呼叫async handleCall(newValue, oldValue) {console.log(newValue, oldValue)if (String(newValue).indexOf('audio') !== -1) {this.mediaType = "audio";} else {this.mediaType = "video";}if (this.ua == null) {this.showLog("发起呼叫:没有ua")return false}if (this.ua.isRegistered() == false) {this.showLog("发起呼叫:未注册")return false}if (this.callee == "") {this.showLog("发起呼叫:被叫号码不能为空")return false;}if (this.callee === this.setting.username) {this.showLog("发起呼叫:不能呼叫自己")return false;}if (this.currentCall) {this.showLog("发起呼叫:已经在通话中")return false;}let options = {eventHandlers: {progress: (e) => {},failed: (e) => {},ended: (e) => {},confirmed: (e) => {},},mediaConstraints: this.mediaType === "video" ? {audio: true,video: true} : {audio: true,video: false},mediaStream: await this.getLocalMediaStream(this.mediaType),pcConfig: {}};console.log("呼出OPTION:", options)try {this.currentCall = toRaw(this.ua).call(`sip:${this.callee}@${this.setting.wssServer}`, options);} catch (error) {console.debug(error)}},//接听电话async handleAnswerCall(newValue, oldValue) {if (!newValue) {return}if (this.currentCall == null) {this.showLog("应答通话,没有通话");}//停止播放来电铃声if (this.ringtone) {this.showLog("应答通话,停止播放来电铃声")this.ringtone.pause();}//开始应答呼let options = {mediaConstraints: this.currentCallMediaType === "video" ? {audio: true,video: true} : {audio: true,video: false},mediaStream: await this.getLocalMediaStream(this.currentCallMediaType),pcConfig: {},};this.showLog("应答通话,options,", options);//应答来电this.currentCall.answer(options);},// 挂断通话handleHangupCall(newValue, oldValue) {console.log(this.currentCall)if (this.currentCall == null) {this.showLog("挂断呼叫:没有通话");}if (this.currentCall) {this.currentCall.terminate();}},// 注册失败时,回调函数onRegistrationFailed(e) {this.showLog("注册失败:", e);this.status.isRegistered = false;},// 注册成功时,回调函数onRegistered(e) {this.showLog("注册成功:", e);this.status.isRegistered = true;//注册成功,跳转到vue home页面this.handleToHome();},// 注销成功时,回调函数onUnregistered(e) {this.showLog("注销成功:", e);this.status.isRegistered = false;},// 连接中时,回调函数onConnecting(e) {this.showLog("正在连接:", e);if (e.attempts >= this.connectCount) {this.showLog("连接失败次数超过5次,停止连接", e);this.handleStop()}},// 服务器断开时,回调函数onDisconnected(e) {this.showLog("断开连接", e);this.status.isRegistered = false;},// 新短信时,回调函数onNewMessage(e) {this.showLog("新短信:", e.originator, e.message, e.request);},// 来电时,回调函数onInComingCall(e) {this.showLog("来电:", e);//获取主叫号码this.ringtone = new Audio(this.ringtoneSound);this.ringtone.loop = true;let play = this.ringtone.play();if (play) {play.then(() => {// 视频频加载成功// 视频频的播放需要耗时setTimeout(() => {// 后续操作this.showLog("来电呼叫,播放来电铃声", this.ringtoneSound);}, this.ringtone.duration * 1000);}).catch((e) => {this.showLog("来电呼叫,呼叫:播放来电铃声失败,", e);})}//判断媒体里是否有视频编码this.currentCallMediaType = this.parseSdp(e.request.body);},// 去电时,回调函数onOutGoingCall(e) {this.showLog("去电:", e);},// ice候选时,回调函数onIceCandidate(e) {this.showLog("ice候选:", e);},// 呼叫中时,回调函数onProgress(e) {this.showLog("呼叫中:", e);this.showLog("当前呼叫:progress-1,", e);this.showLog("当前呼叫:pregress-2:", this.currentCall.connection);if (this.originator === "local") {//去电this.showLog("当前呼叫:progress-3,去电播放被叫回铃音......")//播放180回铃音或者183sdp彩铃(音频彩铃)if (this.currentCall.connection) {let receivers = this.currentCall.connection.getReceivers();this.showLog("当前呼叫:progress-4", receivers)let stream = new MediaStream();stream.addTrack(receivers[0].track);let ringback = new Audio();ringback.srcObject = stream;ringback.play();}} else {//来电this.showLog("当前呼叫:progress-5,来电等待接听......")}},// 呼叫接受时,回调函数onAccepted(e) {this.showLog("呼叫接受:", e);},// PeerConnection时,回调函数onPeerConnection(e) {this.showLog("PeerConnection:", e);},// 呼叫确认时,回调函数onConfirmed(e) {this.showLog("呼叫确认:", e);this.showLog("当前呼叫:confirmed,", this.originator);this.showLog("当前呼叫:confirmed,", this.currentCall.connection);let receivers = this.currentCall.connection.getReceivers();let audioReceiver = null;let videoReceiver = null;// 区分音频和视频接收器receivers.forEach((receiver) => {if (receiver.track.kind === "audio") {audioReceiver = receiver;} else if (receiver.track.kind === "video") {videoReceiver = receiver;}});// 播放音频if (audioReceiver) {this.showLog("播放远端音频")let audioElement = new Audio();let stream = new MediaStream();stream.addTrack(audioReceiver.track);this.audioStream = stream;// 延时播放音频setTimeout(() => {audioElement.srcObject = stream;audioElement.play();}, 500); // 设置音频延时播放 1 秒}// 播放视频if (videoReceiver) {this.showLog("播放远端视频")this.remoteVideoElement = document.createElement('video')// 直接设置内联样式this.remoteVideoElement.style.width = '60%';this.remoteVideoElement.style.height = '60%';this.remoteVideoElement.style.objectFit = 'fill'; // 视频将拉伸以填满容器this.remoteVideoElement.autoplay = truethis.remoteVideoElement.playsinline = truedocument.getElementById(this.remoteVideoElementId).appendChild(this.remoteVideoElement)this.remoteMediaStream = new MediaStream();this.remoteMediaStream.addTrack(videoReceiver.track);// 延时播放视频setTimeout(() => {this.remoteVideoElement.srcObject = this.remoteMediaStream;this.remoteVideoElement.play();}, 500); // 设置视频延时播放 1 秒} else {this.showLog("没有视频流,可能是音频呼叫")}},// 获取sdp时,回调函数onSDP(e) {this.showLog("获取sdp:", e);},// 获取媒体失败时,回调函数onGetUserMediaFailed(e) {this.showLog("获取媒体失败:", e);},// 呼叫结束时,回调函数onEnded(e) {this.releaseMediaStreams();this.showLog("呼叫结束:", e);},// 呼叫失败时,回调函数onFailed(e) {this.releaseMediaStreams();this.showLog("呼叫失败:", e);},// 释放媒体流的方法releaseMediaStreams() {//释放本地视频if (this.localMediaStream) {this.localMediaStream.getTracks().forEach(track => {this.showLog("停止本地媒体流:", track.kind); // 显示是音频还是视频轨道track.stop(); // 停止轨道});this.localMediaStream = null;}// 移除远程video元素if (this.remoteVideoElement) {this.showLog("移除远程video元素");this.remoteVideoElement.remove();this.remoteVideoElement = null;}// 清理本地视频元素(如果有)if (this.localVideoElement) {this.showLog("移除本地video元素");this.localVideoElement.srcObject = null; // 确保不再引用本地流this.localVideoElement = null;}//停止播放铃声if (this.ringtone) {this.showLog("停止播放来电铃声")this.ringtone.pause();}this.currentCall = null;},//解析sdp,获取媒体类型,呼入时使用parseSdp(sdp) {this.showLog("解析SDP:sdp是", sdp)let sb = {};let bs = sdp.split("\r\n");bs.forEach((value, index) => {let a = value.split("=");sb[a[0]] = a[1];});let mediaType = sb.m.split(" ")[0]// mediaType = audio or mediaType = video// 根据不通的类型弹窗this.showLog("解析SDP:媒体类型是", mediaType)return mediaType;},//获取本地媒体 //stream.getTracks() [0]音频 [1]视频.//stream.getAudioTracks() stream.getVideoTracks()// 获取本地流async getLocalMediaStream(mediaType) {try {this.showLog("尝试获取本地媒体流:", mediaType);let constraints = mediaType === "video" ? {audio: true,video: true} : {audio: true,video: false};let stream = await navigator.mediaDevices.getUserMedia(constraints);this.localMediaStream = stream; // 保存本地媒体流,关闭时使用它。this.showLog("获取本地媒体流成功", stream);return stream;} catch (error) {this.showLog('获取本地媒体流失败, 设置虚拟摄像头:', error.name, error.message);// 获取音频流let audioConstraints = {audio: true};let audioStream;try {audioStream = await navigator.mediaDevices.getUserMedia(audioConstraints);} catch (audioError) {this.showLog('获取音频流也失败,仅使用虚拟摄像头:', audioError.name, audioError.message);return this.createVirtualStream(); // 如果音频也获取失败,直接返回虚拟摄像头流}// 获取虚拟摄像头视频流let videoStream = this.createVirtualStream();// 合并音频和视频流let combinedStream = new MediaStream([...audioStream.getTracks(), ...videoStream.getTracks()]);return combinedStream;}},// 创建虚拟摄像头createVirtualStream() {const text = "未找到摄像头设备";const canvas = document.createElement('canvas');canvas.width = 800;canvas.height = 600;const ctx = canvas.getContext('2d');ctx.fillStyle = 'black';ctx.fillRect(0, 0, canvas.width, canvas.height);ctx.fillStyle = 'white';ctx.font = '64px Arial';ctx.textAlign = 'center';ctx.fillText(text, canvas.width / 2, canvas.height / 2);// 将画布内容转换为MediaStreamconst stream = canvas.captureStream();return stream;},// 播放本地媒体流async handleOpenLocalVideo(newValue, oldValue) {if (newValue == false) {this.showLog("初始化数据,跳过");return}this.showLog("打开本地媒体");// 首先获取本地视频if (this.localVideoElementId) {this.showLog("播放视频的控件id:", this.localVideoElementId);if (this.localVideoIsPlaying) {// 已经打开了摄像头,就无需再次打开了this.showLog("本地音频/视频已经打开,无需再次打开");return;} else {try {const stream = await this.getLocalMediaStream("video");this.showLog("视频流为:", stream)if (stream) {this.localVideoElement = document.createElement('video')// 直接设置内联样式this.localVideoElement.style.width = '60%';this.localVideoElement.style.height = '60%';this.localVideoElement.style.objectFit = 'fill'; // 视频将拉伸以填满容器this.localVideoElement.autoplay = truethis.localVideoElement.playsinline = truethis.localVideoElement.srcObject = stream;document.getElementById(this.localVideoElementId).appendChild(this.localVideoElement)this.localVideoIsPlaying = true;} else {this.showLog('无法获取本地视频流:获取到的流为空');}} catch (error) {this.showLog('获取本地视频流失败:', error);if (error.name === 'NotAllowedError') {this.showLog('用户拒绝授予摄像头权限');} else if (error.name === 'NotFoundError') {this.showLog('未找到摄像头设备');} else {this.showLog('其他错误:', error.name, error.message);}}}} else {this.showLog('没有本地视频控件id');}},//关闭本地摄像头handleCloseLocalVideo(newValue, oldValue) {if (newValue == false) {this.showLog("初始化数据,跳过");return}this.showLog("关闭本地视频流");if (this.localMediaStream) {this.localMediaStream.getTracks().forEach((track) => {track.stop();});this.localMediaStream = null;this.localVideoIsPlaying = false; // 更新本地视频播放状态//移除本地video元素if (this.localVideoElement) {this.localVideoElement.remove();this.localVideoElement = null;}} else {this.showLog("没有本地流可以关闭");}},/*** Vue界面相关的操作*///跳转vue界面到home页面,未用到handleToHome() {this.$ownerInstance.callMethod("handleToHome");},//测试piniatest(newValue,oldValue){console.log("我在测试pinia,数据是:",newValue,oldValue);console.log("当前ua的状态:",this.ua?.isRegistered());}},// //监听status,如果发送变化,则更新状态,向vue页面发送状态数据watch: {status: {handler(newValue, oldValue) {console.log("监听status变化,向vue页面发送:", newValue, oldValue);this.$ownerInstance?.callMethod("handleUpdateStatus", newValue);},deep: true},log: {handler(newValue, oldValue) {//console.log("监听log变化,向vue页面发送", newValue, oldValue);this.$ownerInstance?.callMethod("handleUpdateLog", newValue);},deep: true}}
}
store/data.js
import {defineStore
} from 'pinia';export const useSettingStore = defineStore('data', {state: () => {return {formData: {username: "1020",password: "1020",wssServer: "210.51.10.231",wssPort: 7443,mediaType: "audio",callee: "",},action: {register: false,openRemoteVideo: false,closeRemoteVideo: false,openLocalVideo: false,closeLocalVideo: false,audioCall: false,videoCall: false,answerCall: false,hangUpCall: false,stop: false,},status: {data:""}};},
});
pages/index/index.vue
<template><view class="login-container" :register="action.register" :change:register="WebPhone.handleRegister":username="formData.username" :change:username="WebPhone.updateUsername" :password="formData.password":change:password="WebPhone.updatePassword" :wssServer="formData.wssServer":change:wssServer="WebPhone.updateWssServer" :wssPort="formData.wssPort":change:wssPort="WebPhone.updateWssPort" :callee="formData.callee" :change:callee="WebPhone.updateCallee":audioCall="action.audioCall" :change:audioCall="WebPhone.handleCall" :videoCall="action.videoCall":change:videoCall="WebPhone.handleCall" :answerCall="action.answerCall":change:answerCall="WebPhone.handleAnswerCall" :hangupCall="action.hangupCall":change:hangupCall="WebPhone.handleHangupCall" :openLocalVideo="action.openLocalVideo":change:openLocalVideo="WebPhone.handleOpenLocalVideo" :closeLocalVideo="action.closeLocalVideo":change:closeLocalVideo="WebPhone.handleCloseLocalVideo" :stop="action.stop" :change:stop="WebPhone.handleStop"><!-- 厂商Logo或者软件名称 --><text class="software-name">{{ softwareName }}</text><!-- 登录表单 --><uni-forms ref="form" :model="formData"><!-- 分机号 --><uni-forms-item><uni-easyinput v-model="formData.username" placeholder="请输入分机号" /></uni-forms-item><!-- 分机密码 --><uni-forms-item><uni-easyinput v-model="formData.password" placeholder="请输入分机密码" /></uni-forms-item><!-- 服务器地址 --><uni-forms-item><uni-easyinput v-model="formData.wssServer" placeholder="请输入服务器地址" /></uni-forms-item><uni-forms-item><uni-easyinput v-model.number="formData.wssPort" placeholder="请输入服务器端口" /></uni-forms-item><!-- 提交按钮 --><button form-type="submit" class="submit-btn" :disabled="status.data?.isRegistered" @click="handleRegister">登录</button><view>{{ status.data?.isRegistered }}</view><view>{{ log.data }}</view></uni-forms><view class="container"><!-- 远端视频 --><view class="remote-video-container"><view><text class="description">远端视频</text></view><view id="remote-video"></view></view><!-- 本地视频 --><view class="local-video-container"><view><text class="description">本地视频</text></view><view id="local-video"></view></view><!-- 控制按钮:开启、关闭、切换前后摄像头 --><view class="control-buttons"><uni-row><uni-col :span="12"><button @click="openLocalVideo">开启本地视频</button></uni-col><uni-col :span="12"><button @click="closeLocalVideo">关闭本地视频</button></uni-col></uni-row></view><!-- 号码输入框 --><view class="input-container"><uni-easyinput v-model.number="formData.callee" placeholder="请输入被叫号码" /></view><!-- 通话控制按钮:音频、视频通话、挂断 --><view class="call-control-buttons"><uni-row><uni-col :span="6"><button @click="handleAudioCall">音频</button></uni-col><uni-col :span="6"><button @click="handleVideoCall">视频</button></uni-col><uni-col :span="6"><button @click="handleAnswerCall">接听</button></uni-col><uni-col :span="6"><button @click="handleHangupCall">挂断</button></uni-col></uni-row></view><!-- 退出按钮 --><view class="exit-button"><button @click="handleStop">注销</button></view></view></view>
</template><script module="WebPhone" lang="renderjs">import render from "../../utils/render.js";export default render;
</script><script>import {ref,reactive,} from "vue";import {useSettingStore} from "../../store/data";import { storeToRefs } from 'pinia';export default {setup() {const setting = useSettingStore();const formData = setting.formData;const action = setting.action;const status = setting.status;const form = ref(null);//日志const log = reactive({data: ""})//软件名称const softwareName = "MyWssPhone";//修改register值,传给renderjsconst handleRegister = () => {console.log("提交的数据:", formData);action.register = new Date().getTime();};const openLocalVideo = () => {//不变化,不触发renderjs里的方法,所以用date作为每次变化的值console.log("开启本地视频");action.openLocalVideo = new Date().getTime();};const closeLocalVideo = () => {console.log("关闭本地视频");action.closeLocalVideo = new Date().getTime();};const handleAudioCall = () => {console.log("开始音频通话");action.audioCall = "audio" + new Date().getTime();};const handleVideoCall = () => {console.log("开始视频通话");action.videoCall = "video" + new Date().getTime();};const handleAnswerCall = () => {console.log("接听电话")action.answerCall = "answer" + new Date().getTime();}const handleHangupCall = () => {console.log("挂断通话");action.hangupCall = new Date().getTime();};const handleStop = () => {console.log("注销");action.stop = new Date().getTime();};//跳转到首页const handleToHome = () => {console.log("vue页面跳转到home");// uni.navigateTo({// url: "/pages/home/index",// });};//监听renderjs传回的状态const handleUpdateStatus = (data) => {//console.log("vue页面接收到的状态:", data);status.data = data;};//监听renderjs传回的日志const handleUpdateLog = (data) => {//console.log("vue页面接收到的日志:", data);log.data = data;};return {formData,action,status,log,form,softwareName,handleRegister,handleUpdateStatus,handleUpdateLog,handleToHome,openLocalVideo,closeLocalVideo,handleAudioCall,handleVideoCall,handleAnswerCall,handleHangupCall,handleStop,};},};
</script><style scoped>.login-container {padding: 20px;display: flex;flex-direction: column;align-items: center;}.logo,.software-name {margin-bottom: 20px;text-align: center;}.submit-btn {width: 100%;margin-top: 20px;background-color: royalblue;color: white;}.container {display: flex;flex-direction: column;align-items: center;padding: 20px;}.remote-video-container,.local-video-container {text-align: center;}.local-video {width: 350px;/* 容器宽度 */height: 270px;/* 容器高度 */display: flex;justify-content: center;align-items: center;}.remote-video {width: 350px;/* 容器宽度 */height: 270px;/* 容器高度 */display: flex;justify-content: center;align-items: center;}.description {font-size: 12px;}.control-buttons,.call-control-buttons {flex: auto;}.input-container {width: 50%;max-width: 300px;}button {margin: 10px;font-size: 12px;border: none;border-radius: 4px;background-color: #007bff;color: white;cursor: pointer;width: 98%;}button:hover {background-color: #0056b3;}.exit-button button {font-size: 20px;background-color: red;}.exit-button button:hover {background-color: darkred;}
</style>
pages.json
{"pages": [{"path": "pages/index/index","style": {"navigationBarTitleText": "登录"}},{"path": "pages/home/index","style": {"navigationBarTitleText": "首页"}}],"globalStyle": {"navigationBarTextStyle": "black","navigationBarTitleText": "uni-app","navigationBarBackgroundColor": "#F8F8F8","backgroundColor": "#F8F8F8","app-plus": {"background": "#efeff4"}},"condition": {"current": 1,"list": [{"name": "登录","path": "pages/index/index","query": "" }]}
}
3、效果
配置下测试环境,在手机打开app的所有权限,网络、使用音频设备等,看下效果:
Screenrecorder-2024-12-
4、难点
在uniapp上使用JsSIP可以正常进行音视频通信,但是:
APP保活是问题,本人并不了解安卓底层开发,APP运行一段时间后,程序被自动杀掉了,我也沙雕了。
兴趣使然, 仅用于参考,祝君好运