uni-app利用renderjs实现安卓App上jssip+freeswitch+webrtc音视频通话功能

效果图

在这里插入图片描述

前置知识

利用renderjs在app端加载for web库
JsSIP+FreeSwitch+Vue实现WebRtc音视频通话

原始模块

<template><viewclass="test-sip":userExtension="userExtension":change:userExtension="JsSIP.handleUserExtenSionChange":targetExtension="targetExtension":change:targetExtension="JsSIP.handleTargetExtensionChange":logFlag="logFlag":change:logFlag="JsSIP.handleLogFlagChange":jsSipTestLocalStream="jsSipTestLocalStream":change:jsSipTestLocalStream="JsSIP.handleTestLocalStreamChange":jsSipIsRegisted="jsSipIsRegisted":change:jsSipIsRegisted="JsSIP.handleSipRegistedChange":jsSipCallByAudio="jsSipCallByAudio":change:jsSipCallByAudio="JsSIP.handleCallByAudio":jsSipCallByVideo="jsSipCallByVideo":change:jsSipCallByVideo="JsSIP.handleCallByVideo":jsSipHangup="jsSipHangup":change:jsSipHangup="JsSIP.handleJsSipHangupChange"><view class="log-box"><view class="log-item" :style="`color: ${!logFlag?'#2979ff':'#333'}`">关闭日志</view><u-switch class="log-item" v-model="logFlag"></u-switch><view class="log-item" :style="`color: ${logFlag?'#2979ff':'#333'}`">打开日志</view></view><view class="step"><view class="step-title">步骤 1:输入自己的分机号(1001-1019)</view><u--input v-model="userExtension" border="surround" placeholder="请输入自己的分机号(1001-1019)":disabled="hasLocalStream" class="mb-10" :customStyle="{border: '1px solid #e0e0e0'}"/><u-button type="primary" :disabled="!userExtension || isRegisted" @click="registerUser">注册</u-button></view><view class="step"><view class="step-title">步骤 2:输入要呼叫的分机号(1001-1019)</view><u--input v-model="targetExtension" border="surround" placeholder="请输入要呼叫的分机号(1001-1019)" :disabled="!isRegisted"class="mb-10" :customStyle="{border: '1px solid #e0e0e0'}"/><u-button type="primary" class="mb-10" :disabled="!targetExtension || hasCurrentSession" @click="startCall(false)">拨打语音电话</u-button><u-button type="primary" :disabled="!targetExtension || hasCurrentSession" @click="startCall(true)">拨打视频电话</u-button></view><view class="step"><view class="step-title">其他操作</view><u-button type="primary" class="mb-10" :disabled="!hasCurrentSession" @click="jsSipHangup=true">挂断</u-button><u-button type="primary" class="mb-10" :disabled="!isRegisted" @click="jsSipIsRegisted=false">取消注册</u-button><u-button type="primary" v-if="!jsSipTestLocalStream" :disabled="hasCurrentSession" @click="jsSipTestLocalStream=true">测试本地设备</u-button><u-button type="primary" v-else :disabled="hasCurrentSession" @click="jsSipTestLocalStream=false">停止测试本地设备</u-button></view><view class="step" id="audio-container"><!-- <view class="step-title">音频</view> --></view><view class="step" id="video-container"><view class="step-title">视频</view></view><u-notify ref="uNotify"></u-notify></view></template><script>
import { requestCameraPermission, requestRecordAudioPermission } from '@/utils/request-android-permission.js'
export default {data() {return {userExtension: "", // 当前用户分机号targetExtension: "", // 目标用户分机号logFlag: false,hasCurrentSession: false,jsSipTestLocalStream: false,hasLocalStream: false,jsSipIsRegisted: false,isRegisted: false,jsSipCallByAudio: false,jsSipCallByVideo: false,jsSipHangup: false};},mounted() {requestRecordAudioPermission(() => {requestCameraPermission()})},methods: {isValidExtension(extension) {const extNumber = parseInt(extension, 10);return extNumber >= 1001 && extNumber <= 1019;},registerUser() {if (!this.isValidExtension(this.userExtension)) {this.showError("分机号无效,请输入1001-1019之间的分机号");return;}this.jsSipIsRegisted = true},startCall(flag) {if (!this.isValidExtension(this.targetExtension)) {this.showError("分机号无效,请输入1001-1019之间的分机号");return;}flag ? this.jsSipCallByVideo = true : this.jsSipCallByAudio = true},/* 接收 renderjs 传过来的数据 */reciveMessage(msgObj) {console.log('view reciveMsg:', msgObj);const { msg, data } = msgObjswitch (msg) {case 'notify': this.$refs.uNotify[data.type](data.message)breakcase 'changeViewData':this[data.key] = data.value === 'true' ? true : data.value === 'false' ? false : data.value}}},
};
</script><script module="JsSIP" lang="renderjs">
import renderjs from './jsSipRender.js'
export default renderjs
</script><style lang="scss" scoped>
.test-sip {padding: 30px;.log-box {display: flex;margin-bottom: 10px;.log-item {margin-right: 10px;}}.step {margin-bottom: 20px;.mb-10 {margin-bottom: 10px;}.step-title {margin-bottom: 10px;}}}</style>

renderjs 模块

import JsSIP from 'jssip'
const testMp3 = './static/media/test.mp3'
const testMp4 = './static/media/test.mp4'export default {data() {return {userAgent: null, // 用户代理实例incomingSession: null,currentSession: null,outgoingSession: null,password: "xxxxx", // 密码serverIp: "xxxxxxx", // 服务器ipaudio: null,meVideo: null,remoteVideo: null,localStream: null,constraints: {audio: true,video: {width: { max: 1280 },height: { max: 720 },},},myHangup: false,}},computed: {ws_url() {return `ws://${this.serverIp}:5066`;}},mounted() {this.audio = document.createElement('audio')this.audio.autoplay = true// this.audio.src = testMp3document.getElementById('audio-container').appendChild(this.audio)this.meVideo = document.createElement('video')this.meVideo.autoplay = truethis.meVideo.playsinline = true// this.meVideo.src = testMp4document.getElementById('video-container').appendChild(this.meVideo)this.remoteVideo = document.createElement('video')this.remoteVideo.autoplay = truethis.remoteVideo.playsinline = true// this.remoteVideo.src = testMp4document.getElementById('video-container').appendChild(this.remoteVideo)const styleObj = {width: '150px','background-color': '#333',border: '2px solid blue',margin: '0 5px'}Object.keys(styleObj).forEach(key => {this.meVideo.style[key] = styleObj[key]this.remoteVideo.style[key] = styleObj[key]})},methods: {handleLogFlagChange(nV, oV) {nV ? JsSIP.debug.enable("JsSIP:*") : JsSIP.debug.disable("JsSIP:*");// this.log('logFlag', nV, oV)/* if(oV !== undefined) {this.log('logFlag', nV, oV)} */},handleUserExtenSionChange(nV, oV) {if(oV !== undefined) {// this.log('userExtenSion', nV, oV)}},handleTargetExtensionChange(nV, oV) {if(oV !== undefined) {// this.log('targetExtenSion', nV, oV)}},handleTestLocalStreamChange(nV, oV) {if(oV !== undefined) {// this.log('jsSipTestLocalStream', nV, oV)if(nV) {this.captureLocalMedia(() => {this.sendMsg('changeViewData', {key: 'hasLocalStream',value: true})}, (e) => {this.sendMsg('changeViewData', {key: 'jsSipTestLocalStream',value: false})this.sendMsg('notify', {type: 'error',message: "getUserMedia() error: " + e.name})})} else {this.stopLocalMedia()this.sendMsg('changeViewData', {key: 'jsSipTestLocalStream',value: false})}}},handleSipRegistedChange(nV, oV) {if(oV !== undefined) {if(nV) {this.registerUser()} else {this.unregisterUser()}}},handleCallByAudio(nV, oV) {if(oV !== undefined) {if(nV) {this.startCall(false)}}},handleCallByVideo(nV, oV) {if(oV !== undefined) {if(nV) {this.startCall(true)}}},handleJsSipHangupChange(nV, oV) {if(oV !== undefined) {if(nV) {this.hangUpCall()}}},captureLocalMedia(successCb, errCb) {console.log("Requesting local video & audio");navigator.mediaDevices.getUserMedia(this.constraints).then((stream) => {console.log("Received local media stream");this.localStream = stream;// 连接本地麦克风if ("srcObject" in this.audio) {this.audio.srcObject = stream;} else {this.audio.src = window.URL.createObjectURL(stream);}// 如果有视频流,则连接本地摄像头if (stream.getVideoTracks().length > 0) {if ("srcObject" in this.meVideo) {this.meVideo.srcObject = stream;} else {this.meVideo.src = window.URL.createObjectURL(stream);}}successCb()}).catch((e) => errCb(e));},stopLocalMedia() {if (this.localStream) {this.localStream.getTracks().forEach((track) => track.stop());this.localStream = null;// 清空音频和视频的 srcObjectthis.clearMedia("audio");this.clearMedia("meVideo");}},clearMedia(mediaNameOrStream) {let mediaSrcObject = this[mediaNameOrStream].srcObject;if (mediaSrcObject) {let tracks = mediaSrcObject.getTracks();for (let i = 0; i < tracks.length; i++) {tracks[i].stop();}}this[mediaNameOrStream].srcObject = null;},registerUser() {const configuration = {sockets: [new JsSIP.WebSocketInterface(this.ws_url)],uri: `sip:${this.userExtension}@${this.serverIp};transport=ws`,password: this.password,contact_uri: `sip:${this.userExtension}@${this.serverIp};transport=ws`,display_name: this.userExtension,register: true, //指示启动时JsSIP用户代理是否应自动注册session_timers: false, //关闭会话计时器(根据RFC 4028)};this.userAgent = new JsSIP.UA(configuration);this.userAgent.on("connecting", () => console.log("WebSocket 连接中"));this.userAgent.on("connected", () => console.log("WebSocket 连接成功"));this.userAgent.on("disconnected", () =>console.log("WebSocket 断开连接"));this.userAgent.on("registered", () => {console.log("用户代理注册成功");this.sendMsg('changeViewData', { key: 'isRegisted', value: true })});this.userAgent.on("unregistered", () => {console.log("用户代理取消注册");this.sendMsg('changeViewData', { key: 'isRegisted', value: false })});this.userAgent.on("registrationFailed", (e) => {this.sendMsg('notify', { type: 'error', message: '注册失败' })});this.userAgent.on("newRTCSession", (e) => {console.log("新会话: ", e);if (e.originator == "remote") {console.log("接听到来电");this.incomingSession = e.session;this.sipEventBind(e);} else {console.log("打电话");this.outgoingSession = e.session;this.outgoingSession.on("connecting", (data) => {console.info("onConnecting - ", data.request);this.currentSession = this.outgoingSession;this.sendMsg('changeViewData', { key: 'hasCurrentSession', value: true })this.outgoingSession = null;});this.outgoingSession.connection.addEventListener("track", (event) => {console.info("Received remote track:", event.track);this.trackHandle(event.track, event.streams[0]);});//连接到信令服务器,并恢复以前的状态,如果以前停止。重新开始时,如果UA配置中的参数设置为register:true,则向SIP域注册。this.userAgent.start();console.log("用户代理启动");}})//连接到信令服务器,并恢复以前的状态,如果以前停止。重新开始时,如果UA配置中的参数设置为register:true,则向SIP域注册。this.userAgent.start();console.log("用户代理启动");},startCall(isVideo = false) {if (this.userAgent) {try {const eventHandlers = {progress: (e) => console.log("call is in progress"),failed: (e) => {console.error(e);this.sendMsg('notify', {type: 'error',message: `call failed with cause: ${e.cause}`})},ended: (e) => {this.endedHandle();console.log(`call ended with cause: ${e.cause}`);},confirmed: (e) => console.log("call confirmed"),};console.log("this.userAgent.call");this.outgoingSession = this.userAgent.call(`sip:${this.targetExtension}@${this.serverIp}`, // :5060{mediaConstraints: { audio: true, video: isVideo },eventHandlers,});} catch (error) {this.sendMsg('notify', {type: 'error',message: '呼叫失败'})console.error("呼叫失败:", error);}} else {this.sendMsg('notify', {type: 'error',message: '用户代理未初始化'})}},sipEventBind(remotedata, callbacks) {//接受呼叫时激发remotedata.session.on("accepted", () => {console.log("onAccepted - ", remotedata);if (remotedata.originator == "remote" && this.currentSession == null) {this.currentSession = this.incomingSession;this.sendMsg('changeViewData', { key: 'hasCurrentSession', value: true })this.incomingSession = null;console.log("setCurrentSession:", this.currentSession);}});//在将远程SDP传递到RTC引擎之前以及在发送本地SDP之前激发。此事件提供了修改传入和传出SDP的机制。remotedata.session.on("sdp", (data) => {console.log("onSDP, type - ", data.type, " sdp - ", data.sdp);});//接收或生成对邀请请求的1XX SIP类响应(>100)时激发。该事件在SDP处理之前触发(如果存在),以便在需要时对其进行微调,甚至通过删除数据对象中响应参数的主体来删除它remotedata.session.on("progress", () => {console.log(remotedata);console.log("onProgress - ", remotedata.originator);if (remotedata.originator == "remote") {console.log("onProgress, response - ", remotedata.response);//answer设置的自动接听//RTCSession 的 answer 方法做了自动接听。实际开发中,你需要弹出一个提示框,让用户选择是否接听const isVideoCall = remotedata.request.body.includes("m=video");const flag = confirm(`检测到${remotedata.request.from.display_name}${isVideoCall ? "视频" : "语音"}来电,是否接听?`);if(!flag) {this.hangUpCall();return;} else {//如果同一电脑两个浏览器测试则video改为false,这样被呼叫端可以看到视频,两台电脑测试让双方都看到改为trueremotedata.session.answer({mediaConstraints: { audio: true, video: isVideoCall },// mediaStream: this.localStream,});}}});//创建基础RTCPeerConnection后激发。应用程序有机会通过在peerconnection上添加RTCDataChannel或设置相应的事件侦听器来更改peerconnection。remotedata.session.on("peerconnection", () => {console.log("onPeerconnection - ", remotedata.peerconnection);if (remotedata.originator == "remote" && this.currentSession == null) {//拿到远程的音频流/* remotedata.session.connection.addEventListener("addstream",(event) => {console.info("Received remote stream:", event.stream);this.streamHandle(event.stream);}); */remotedata.session.connection.addEventListener("track", (event) => {console.info("Received remote track:", event.track);this.trackHandle(event.track, event.streams[0]);});}});//确认呼叫后激发remotedata.session.on("confirmed", () => {console.log("onConfirmed - ", remotedata);if (remotedata.originator == "remote" && this.currentSession == null) {this.currentSession = this.incomingSession;this.sendMsg('changeViewData', { key: 'hasCurrentSession', value: true })this.incomingSession = null;console.log("setCurrentSession - ", this.currentSession);}});// 挂断处理remotedata.session.on("ended", () => {this.endedHandle();console.log("call ended:", remotedata);});remotedata.session.on("failed", (e) => {this.sendMsg('notify', { type: 'error', message: '会话失败' })console.error("会话失败:", e);});},unregisterUser() {console.log("取消注册");this.userAgent.unregister();this.sendMsg('changeViewData', { key: 'isRegisted', value: false })this.sendMsg('changeViewData', { key: 'userExtension', value: '' })this.sendMsg('changeViewData', { key: 'targetExtension', value: '' })},trackHandle(track, stream) {const showVideo = () => {navigator.mediaDevices.getUserMedia({...this.constraints,audio: false, // 不播放本地声音}).then((stream) => {this.meVideo.srcObject = stream;}).catch((error) => {this.sendMsg('notify', {type: 'error',message: `${error.name}${error.message}`})});};// 根据轨道类型选择播放元素if (track.kind === "video") {// 使用 video 元素播放视频轨道this.remoteVideo.srcObject = stream;showVideo();} else if (track.kind === "audio") {// 使用 audio 元素播放音频轨道this.audio.srcObject = stream;}},endedHandle() {this.clearMedia("meVideo");this.clearMedia("remoteVideo");this.clearMedia("audio");if (this.myHangup) {this.sendMsg('notify', { type: 'success', message: '通话结束' })} else {this.sendMsg('notify', { type: 'warning', message: '对方已挂断!' })}this.myHangup = false;this.currentSession = null;this.sendMsg('changeViewData', { key: 'hasCurrentSession', value: false })this.sendMsg('changeViewData', { key: 'jsSipCallByVideo', value: false })this.sendMsg('changeViewData', { key: 'jsSipCallByAudio', value: false })},hangUpCall() {this.myHangup = true;this.outgoingSession = this.userAgent.terminateSessions();this.currentSession = null;this.sendMsg('changeViewData', { key: 'hasCurrentSession', value: false })this.sendMsg('changeViewData', { key: 'jsSipHangup', value: false })},// 日志log(key, nV, oV) {console.log(`renderjs:${key} 改变`);console.log(`${key} 新值:`, nV);console.log(`${key} 旧值:`, oV);},// 向视图层发送消息sendMsg(msg, data) {// 向页面传参// console.log('renderjs sendMsg:');// console.log(msg, data);this.$ownerInstance.callMethod('reciveMessage', {msg,data})},}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/26446.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python邮箱发送如何设置?Python发信方法?

Python邮箱发送邮件需要哪些库&#xff1f;怎么使用Python发信&#xff1f; Python的强大之处在于其丰富的库和模块&#xff0c;使得开发者可以轻松地实现各种功能&#xff0c;包括通过电子邮件发送信息。AokSend将介绍如何在Python中设置和发送电子邮件&#xff0c;以及相关的…

超高频载码体有哪些特点?

载码体由线圈、已编程的芯片&#xff0c;以及电池(在有源读写系统中)组成&#xff0c;具有唯一的电子编码&#xff0c;拥有大容量的存储空间&#xff0c;通常附着于产品载体乃至是产品本身&#xff0c;成为一个随产品移动的移动数据库&#xff0c;可以帮助企业更好的物料、成品…

AH8652:220V转5V非隔离电源芯片

### AH8652&#xff1a;220V转5V非隔离电源芯片&#xff0c;高效转换新选择 #### 引言 随着电子设备对电源稳定性和安全性要求的提高&#xff0c;非隔离电源转换芯片因其简单、高效和成本效益而受到市场的欢迎。AH8652是一款专为220V转5V设计的非隔离电源芯片&#xff0c;以其…

Ubuntu安装Protobuf

以前的版本中&#xff0c;有./configure&#xff0c;所以参照下面的博客链接 Ubuntu安装Protobuf&#xff0c;指定版本_ubuntu更新protobuf-CSDN博客 后来的版本中&#xff0c;没有了./configure文件&#xff0c;需要安装bazel,参照下面的官网链接 protobuf/src/README.md a…

基于 Redis 实现分布式缓存

一、单节点 Redis 的问题 1.1 存在的问题 1、数据丢失问题&#xff1a;Redis 是内存存储&#xff0c;服务重启可能会丢失数据。 2、并发能力问题&#xff1a;单节点 Redis 并发能力虽然不错&#xff0c;但也无法满足如 618 这样的高并发场景。 3、故障恢复问题&#xff1a;如果…

实践分享:鸿蒙跨平台开发实例

先来理解什么是跨平台 提到跨平台&#xff0c;要先理解什么是“平台”&#xff0c;这里的平台&#xff0c;就是指应用程序的运行环境&#xff0c;例如操作系统&#xff0c;或者是Web浏览器&#xff0c;具体的像HarmonyOS、Android、iOS、或者浏览器&#xff0c;都可以叫做平台…

用于云医疗图像的缩略图保持加密方案

论文标题&#xff1a;《Data hiding with thumbnail-preserving encryption for cloud medical images》&#xff0c;作者提出了一种用于云医疗图像的可逆数据隐藏方案&#xff0c;同时保留了缩略图。下面是论文的创新点和算法过程的总结。 一、缩略图保持加密与传统图像加密 …

GD32 MCU超频后无法再次下载程序的解决办法

我们知道&#xff0c;MCU的系统时钟主频就相当于人的心跳或脉搏&#xff0c;为所有的工作单元提供时间基数&#xff0c;所以一般在程序最开始的地方都需要进行主频配置。 GD32固件库中提供了多种宏定义&#xff0c;可以很方便的将系统时钟配置为想要的频率。 GD32固件库中所用…

AI宣传文案软件有哪些?5款AI软件推荐

AI宣传文案软件有哪些&#xff1f;AI宣传文案软件在现代营销和创意产业中扮演着越来越重要的角色&#xff0c;它们凭借先进的自然语言处理、机器学习和深度学习技术&#xff0c;不仅解放了创作者的双手&#xff0c;还大大提升了文案的生成效率和质量。这些软件能够精准捕捉用户…

python快速入门之Flask框架

文章目录 一、pip安装二、接口开发三、测试 一、pip安装 pip install flask 二、接口开发 from flask import Flaskapp Flask(__name__)app.route("/test") def index():return "test"if __name__ __main__:app.run()三、测试 http://127.0.0.1:5000…

redis 08 慢查询日志

1.什么是慢查询日志 2.慢查询和两个参数有关 2.1 2.2 3.例子&#xff1a; 4 参数详细介绍&#xff1a;

ICRA 2024:基于视觉触觉传感器的物体表⾯分类的Sim2Real双层适应⽅法

⼈们通常通过视觉来感知物体表⾯的性质&#xff0c;但有时需要通过触觉信息来补充或替代视觉信息。在机器⼈感知物体属性⽅⾯&#xff0c;基于视觉的触觉传感器是⽬前的最新技术&#xff0c;因为它们可以产⽣与表⾯接触的⾼分辨率 RGB 触觉图像。然⽽&#xff0c;这些图像需要⼤…

如何通过亚马逊测评提升产品竞争力的关键策略

在亚马逊这个全球领先的跨境电商平台上&#xff0c;随着卖家数量的激增&#xff0c;产品间的竞争愈发激烈&#xff0c;为了在市场中脱颖而出&#xff0c;提高产品的竞争力成为了每位卖家必须面对的问题&#xff0c;而在这其中&#xff0c;亚马逊测评作为一种有效的市场策略&…

STM32项目分享:车牌号识别系统

目录 一、前言 二、项目简介 1.功能详解 2.主要器件 三、原理图设计 四、PCB硬件设计 1.PCB图 2.PCB板打样焊接图 五、程序设计 六、实验效果 七、资料内容 项目分享 一、前言 项目成品图片&#xff1a; 哔哩哔哩视频链接&#xff1a; https://www.bilibili.…

python数据分析-房价数据集聚类分析

一、研究背景和意义 随着房地产市场的快速发展&#xff0c;房价数据成为了人们关注的焦点。了解房价的分布特征、影响因素以及不同区域之间的差异对于购房者、房地产开发商、政府部门等都具有重要的意义。通过对房价数据的聚类分析&#xff0c;可以深入了解房价的内在结构和规…

Flutter 简化线程Isolate的使用

文章目录 前言一、完整代码二、使用示例1、通过lambda启动线程2、获取线程返回值3、线程通信4、结束isolate 总结 前言 flutter的线程是数据独立的&#xff0c;每个线程一般通过sendport来传输数据&#xff0c;这样使得线程调用没那么方便&#xff0c;本文将提供一种支持lambd…

音频处理1_基本概念

AI变声和音乐创作的基础 声音本质上是人类可察觉范围内的气压周期性波动, 即声波 声波是一种连续信号&#xff0c;在任意时间内的声音信号有无数个取值。对于只能读取有限长数组计算机来说&#xff0c;我们需要将连续的声音信号转换为一个离散的序列&#xff0c;即数字化表示。…

法考报名必看,99%高过审率证件照片电子版制作技巧

在2024年&#xff0c;法考备战已经如火如荼进行中&#xff0c;作为进入法律行业的第一步&#xff0c;参加法考的重要性不言而喻。而作为报名过程中必不可少的一环&#xff0c;报名照片要求以及证件照制作技巧更是需要我们特别重视的部分。想要在这个过程中顺利通过审核&#xf…

【全开源】图书借阅管理系统源码(ThinkPHP+FastAdmin)

&#x1f4da;图书借阅管理系统&#xff1a;打造你的私人图书馆 一款基于ThinkPHPFastAdmin开发的简易图书借阅管理系统&#xff0c;一款轻量级的图书借阅管理系统&#xff0c;具有会员管理&#xff0c;图书管理&#xff0c;借阅及归还管理&#xff0c;会员充值等基本功能&…