FreeSWITCH 简单图形化界面38 - 使用uniapp中使用JsSIP进行音视频呼叫
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 = false
App.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,
// 初始化 ua
MyCaller: 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的socket
socket: 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 = true
this.remoteVideoElement.playsinline = true
document.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);
// 将画布内容转换为MediaStream
const 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 = true
this.localVideoElement.playsinline = true
this.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");
},
//测试pinia
test(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值,传给renderjs
const 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运行一段时间后,程序被自动杀掉了,我也沙雕了。
兴趣使然, 仅用于参考,祝君好运