概述
上节完成vue3项目中,如何引入阿里图标的问题。其最终效果下图:
嘟宝是为监督孩子身边环境而创建的一套应用,它能够实现后台驻留长连接,随时接收嘟妈信令,建立音视频通道点对点通信,而不需要中转服务器。嘟妈是一套WEB 应用,使用vue3编写,能够兼容app、小程序开发 。而嘟宝采用原生Andorid开发,能够获取最大权限,保持在后台运行。
目前为止,嘟宝完成的基本功能设计包括
- MQTT 基础通信
- 后台驻留
- SQLite数据存储
- 二维码显示身份识别码
- 开启自启动前台服务
- 任务栏消息发送与点击
- 高德定位实时更新
- webrtc点对点通信
- mAgentWeb控件封装解决全屏与获取视频权限问题
嘟妈采用vue3编写它的核心需求已完成包含以下部分: - 完成登录注册页面
- 完成二维码扫描完成与嘟宝绑定
- 与嘟妈建立音视频通信
- 查看嘟宝桌面共享视频
项目地址在github上。
嘟宝地址
嘟妈地址
在dobao.ts中混在许多内容,如mqtt通信,webrtc通信、pinna存储,不利于代码耦合与重复利用。设计一个工具类dubaoclient,用于webrtc通信
设计 dubaoclient
dubaoclient类位于src\utils\dubaoclient.ts文件中,其初始实现如下:
exportclassdubaoclient{dubaoId:string;dumaId:string;constructor(dumaId:string,dubaoId:string){this.dubaoId=dumaId;this.dumaId=dubaoId;console.log("dubao constructor");}test(){console.log("dubao test");}}在dubao.ts中引用并测试该类:
import{dubaoclient}from'@/utils/dubao'constclient=newdubaoclient('1','2')client.test();运行效果如下图所示:
传参
在dumaclient构造函数中,包含两个参数,dubaoId、dumaId,若存在很多参数,如何解决构造函数传参问题。
dubaoclient源码
export classdubaoclient{dubaoId!:string;dumaId!:string;constructor(data:Partial<dubaoclient>){Object.assign(this,data)console.log("dubao constructor");}test(){console.log("dubaoId:",this.dubaoId);console.log("dumaId:",this.dumaId);}}dubao.ts引用
import{dubaoclient}from'@/utils/dubao'const client=newdubaoclient({dubaoId:'8206aa74-69c5-483e-83b5-16616e3030eb',dumaId:'f1122aeb-f2b0-400d-9919-eddd2eaebaa2'})client.test();演示结果
回调函数
dubaoclient产生事件后,向数据传递给调用者,这就需要用到回调函数
export classdubaoclient{dubaoId!:string;dumaId!:string;peerConnection!:any dataChannel!:any private pushmsg!:(type:string,msg:string)=>Promise<void>constructor(data:Partial<dubaoclient>,pushmsg:(type:string,msg:string)=>Promise<void>){Object.assign(this,data)this.pushmsg=pushmsg console.log("dubao constructor");}test(){console.log("dubaoId:",this.dubaoId);console.log("dumaId:",this.dumaId);}}dubao.ts源码
import{dubaoclient}from'@/utils/dubao'const client=newdubaoclient({dubaoId:'8206aa74-69c5-483e-83b5-16616e3030eb',dumaId:'f1122aeb-f2b0-400d-9919-eddd2eaebaa2'},pushmsg)client.test();asyncfunctionpushmsg(code:any,d:any){console.log('pushmsg',code,d)}效果如下图
重构dubaoclient源码
export classdubaoclient{// dubaoId!: string;// dumaId!: string;private peerConnection!:any private dataChannel!:any private pushmsg!:(type:string,msg:string)=>Promise<void>constructor(data:Partial<dubaoclient>,pushmsg:(type:string,msg:string)=>Promise<void>){Object.assign(this,data)this.pushmsg=pushmsg console.log("dubao constructor");}test(){// console.log("dubaoId:", this.dubaoId);// console.log("dumaId:", this.dumaId);this.pushmsg('test','test message');}asyncstart(coturnurl:string){const configuration={iceServers:[{urls:coturnurl}],}let peerConnection=newRTCPeerConnection(configuration)this.peerConnection=peerConnection;// ✅ 创建数据通道(必须在创建 Offer 之前!)let dataChannel=peerConnection.createDataChannel('chat',{ordered:true,// 保证顺序maxRetransmits:3,// 最大重传次数(不设置则可靠传输)// 或者使用 maxPacketLifeTime: 1000 // 最大存活时间(毫秒)})this.dataChannel=dataChannel;// 监听数据通道事件dataChannel.onopen=()=>{console.log(' 数据通道已打开')dataChannel.send('Hello from offerer!')}dataChannel.onmessage=(event:any)=>{console.log('收到消息:',event.data)}dataChannel.onclose=()=>{console.log('数据通道已关闭')}peerConnection.addEventListener('icecandidate',async(event:any)=>{if(event.candidate){await this.pushmsg('ice',JSON.stringify(event.candidate))}})try{const localStream=await navigator.mediaDevices.getUserMedia({video:true,audio:true,})localStream.getTracks().forEach((track)=>{console.log('添加本地轨道:',track)// peerConnection.addTrack(track, localStream);})}catch(error){console.log(error)}// 2. 创建Offerconst offer=await peerConnection.createOffer({offerToReceiveAudio:true,offerToReceiveVideo:true,})await peerConnection.setLocalDescription(offer)await this.pushmsg('offer',JSON.stringify(offer))// 监听远程流(视频通话场景)peerConnection.addEventListener('track',(event:any)=>{const remoteVideo:any=document.getElementById('remoteVideo')console.log(remoteVideo.srcObject)if(remoteVideo.srcObject){const stream:any=remoteVideo.srcObjectif(event.track.kind=='video'){try{stream.getVideoTracks()stream.removeTrack(stream.getVideoTracks()[0])}catch(error){console.log(error)}}remoteVideo.srcObject.addTrack(event.track)}else{remoteVideo.srcObject=newMediaStream(event.streams[0])}remoteVideo.play()})// 连接状态变化监听peerConnection.addEventListener('connectionstatechange',()=>{console.log('Connection state:',peerConnection.connectionState)if(peerConnection.connectionState=='disconnected'){this.start(coturnurl)}})}asyncsetRemoteDescription(data:any){await this.peerConnection.setRemoteDescription(newRTCSessionDescription(JSON.parse(data)))}asyncaddIceCandidate(data:any){await this.peerConnection.addIceCandidate(newRTCIceCandidate(JSON.parse(data)))}bye(){if(this.peerConnection){// 关闭所有发送器this.peerConnection.getSenders().forEach((sender:any)=>{if(sender.track)sender.track.stop()})// 关闭连接this.peerConnection.close()this.peerConnection=null}}}dubao.ts源码
<template><div class="container"><van-nav-bar:title="dubao?.dubaoName || '嘟宝'"left-text="返回"left-arrow @click-left="goback"><template #right><div class="nav-right"@click="showMenu = true"><van-icon name="ellipsis"size="18"/></div></template></van-nav-bar><van-action-sheet v-model:show="showMenu":actions="menuActions"cancel-text="取消"@select="onMenuSelect"/><div class="video-container"><video id="remoteVideo"controls controlslist="noremoteplayback fullscreen"></video></div><div class="video-controls"><span class="control-btn"><svg class="icon"aria-hidden="true"><use xlink:href="#icon-ziyuan"></use></svg></span><span class="control-btn"><svg class="icon"aria-hidden="true"><use xlink:href="#icon-guanbishengyin"></use></svg></span><span class="control-btn"><svg class="icon"aria-hidden="true"><use xlink:href="#icon-xiaoxi"></use></svg></span><span class="control-btn"><svg class="icon"aria-hidden="true"><use xlink:href="#icon-dingwei"></use></svg></span></div></div></template><script setup lang="ts">import{onMounted,onUnmounted,ref}from'vue'import{c}from'@/myconfig'import{useMQTTStore}from'@/store/mqtt'import{useRoute,useRouter}from'vue-router'import{useDubaoStore}from'@/store/dubao'import{dubaoclient}from'@/utils/dubao'const client=newdubaoclient({},pushmsg)let store=useDubaoStore()const route=useRoute()const router=useRouter()const{dubaoId}=route.params const dubao=store.getDuBao(dubaoId as string)const mqtt=useMQTTStore()const showMenu=ref(false)const menuActions=[{name:'呼叫',icon:'phone-o',action:'call'},{name:'切换相机',icon:'replay',action:'changecam'},{name:'远程桌面',icon:'desktop-o',action:'changescreen'},{name:'挂断',icon:'cross',action:'bye',color:'#ee0a24'},]let isMuted=falselet peerConnection:any let dataChannel:any=nullmqtt.$subscribe(async(mutate:any,state)=>{if(typeof mutate.events.newValue==='boolean'){console.log('是布尔类型')return}let data=JSON.parse(state.datamsg)let msg=JSON.parse(data.msg)switch(msg.code){case'answer':// await peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.data)))client.setRemoteDescription(msg.data);breakcase'ice':// await peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.data)))client.addIceCandidate(msg.data);default:break}})onMounted(()=>{})onUnmounted(()=>{client.bye()})functiongoback(){// history.back();router.push('/main')}asyncfunctionpushmsg(code:any,d:any){await mqtt.pushmsg(dubaoId as string,code,d)}functioncall(){// init()client.start(c.coturnurl);}</script>演示效果如下