写在开头,本文是在开发过程中做的随手记录,可能需要一些相关知识的基础
一、简单介绍 WebRTC
WebRTC( Real-Time Communications)是一个可以在 Web 应用程序中实现音频、视频和数据的实时通信的开源项目,它封装了很多音视频的采集、处理功能,比如音视频流的编解码、降噪和回声消除等。通过WebRTC,我们可以方便地获取优化后的媒体流,将其输入到本地或转发给其他对等端。
二、项目情况大概介绍
(有兴趣的话,可以点击这里)
如果有使用我们产品 WebCast 的话,要记住,在使用过程中用到的 分享码 / 连接码 只可以给你们相信的人进行连接哦!!!!!
技术:vue3 + typeScript + elementUI
大概介绍:本项目是单向的屏幕分享 + 双向语音交流。
三、简单的连接流程
1. 建立 WebSocket()
WebSocket 实现了浏览器与服务器之间的全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
主要职能是:发送自身的心跳消息、接受对方的心跳消息、确认对方是否在线、是否可以开始连接 以及 对方一些连接状态值的改变
确认对方设备在线后,开始以下步骤
2.初始化 RTCPeerConnection()
RTCPeerConnection 接口代表一个由本地计算机到远端的 WebRTC 连接。该接口提供了创建,保持,监控,关闭连接的方法的实现。
主要职能是:创建通道,互相设置彼此的信息(SDP、ICE),生成自己的直播地址,是webrtc的核心。
这边可以通过监听 RTCPeerConnection 提供的各类事件,更好的完成整个webrtc的连接流程
关于监听 RTCPeerConnection 事件时遇见的问题: 官方文档中 RTCPeerConnection / iceConnectionState 有一个 completed 状态值,但是我在实际开发过程中,并没有获取到,这个还不知道原因。后面改成监听 icegatheringstatechange 的值,当iceGatheringState === 'completed' 视为连接成功
3.创建音、视频数据 getUserMedia()
这边先创建音频数据,再初始化RTCPeerConnection也可以,只要是在 SDP 交换之前创建就行
不过后来我们在开发过程中,为了产品可以有更好的用户体验,我们找到了其他更好的方式: 在SDP交换的时候,是携带一段默认的空视频(或白噪音音频)交付给客户端,等到连接建立完成后再调用 getUserMedia(),将音轨替换(具体写在后面兼容处理部分中)
主要职能是:设置一个可用的音、视频轨道,用来传输音、视频内容。
4.初始化 MQTT
MQTT是一个基于客户端-服务器的消息发布/订阅传输的消息协议
主要职能是:建立通道,订阅消息,确定双方执行到哪一步,并及时给对方所需信息
这边使用 MQTT over WebSocket 的原因:
标准的 MQTT 是通过 TCP 协议来进行通信的,这样网页就没法使用MQTT协议了。一个变通的方法是,在同一个程序中,同时集成 MQTT 服务和一个 WebSocket 服务,通过 WebSocket 服务,将 MQTT 服务收到的消息转发给网页,这样网页就也能使用 MQTT协议 了
四、开发过程中逻辑代码部分兼容处理
1. 关于音频数据交换处理
这边的处理方式并非传统的,先执行 getUserMedia() 获取音频内容,再建立通道的方式
而是 先用一段白噪音代替执行 getUserMedia() 获取到的内容去建立通道,等到真正需要音频的时候再用getUserMedia() 获取到的内容,去替代最先的白噪音内容
当然这边是由于该项目的特殊性,可能对于其他项目来说这个方式并不是最优解
// 这边之所以不用 new Audio 的方式,而是采用 html dom 的方式,主要是为了使用 autoplay 的属性,让这段音频提前先播放完毕(autoplay 的属性直接写在标签上,在部分浏览器上会失效)
// 这边如果是视频的话就用 video 标签
let audio:any = document.createElement('audio')
//最好是让用户无感知的白噪音音频
audio.src = ('xxxx')
audio.autoplay = true
const addTrack = () => {
let audioStream = audio.captureStream ? audio.captureStream() : audio.mozCaptureStream ? audio.mozCaptureStream() : ''
if(audioStream){
// 这边一定要等待彻底播放,才会有 audio track
let audioTrack = audioStream.getAudioTracks()[0]
this.#mediaStream.addTrack(audioTrack)
// 添加本地视频轨到 peerConnection 之中
this.#mediaStream.getTracks().forEach(track => {
this.rtcModel.getPc().addTrack(track, this.#mediaStream)
})
}
}
//这边需要加一个定时器,确保自动播放的音频一定是播完的,不然会报错
setTimeout(async () => {
try {
addTrack()
} catch (e){
try{
await audio.play()
addTrack()
}catch (error:any) {
console.log(error)
}
}
},150)
// 在需要使用时再调用该方法
navigator.mediaDevices.getUserMedia({ video: false, audio: true }).then(stream => {
let audioTrack = stream.getAudioTracks()[0]
let trackSender = this.#pc.getSenders().find(s => s.track.kind === audioTrack.kind)
trackSender?.replaceTrack(audioTrack)
}).catch(e => {})
注意点:
(1)如果在建立webrtc之前就先去获取video相关信息,再进行 createoffer 会出现问题,会导致ice收集不到,即根本不会onIceCandidate相关事件根本不会执行
这时候可以添加相关的适配文件 webrtc.github.io/adapter/ada…
(2)使用本文提到的,先获取到空音、视频的track信息,后期再进行对应信息替换的方法时,要注意 safari浏览器并不兼容 captureStream(),所以该方法在safari上并不适用
(3) mozCaptureStream 在win10上有兼容问题,只要浏览器的麦克风权限打开了,无论有没有插耳机,系统的麦克风权限有没有开,都会认为是获取到了麦克风权限。暂时没有找到解决办法,所以这边要注意如果存在captureStream方法,要先走captureStream方法
(4) safari 这边有一点需要注意,如果在建立webrtc连接之前,就将streams的声音禁用的话,safari会默认该stream是不带音频的,后面也无法打开
2.关于video 视频播放的兼容处理
<video class="video" id="video" draggable="false" muted></video>
let video = document.getElementById("video")
// mediaStream 为要显示的视频内容
video.srcObject = mediaStream
video.playsInline = true
video.play()?.catch(e => {
// 刚开始初始化的时候,要禁音,不然 chrome 会报这个错误 Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first.
// 这边再次执行静音操作是由于,有些浏览器会无视dom上的静音设置
video.muted = true
video.play()?.catch(e => {
// 这边需要注意,依旧存在部分设备 必须用户操作后才可以进行视频播放(例如华为平板)
})
})
3. 监听浏览器的刷新操作
因为是浏览器连接,所以一旦监听到刷新的操作,连接应该立即断开。这边需要针对火狐浏览器的刷新操作进行特殊的处理,其他浏览器在刷新的时候,我们监听到了之后是可以来得及发通知给对方,告诉对方连接应该断开了。
但是火狐的刷新功能,好像是没有办法等待我们发通知给对方,所以这边用到的处理方式是,是刷新的时候存一个标志值到sessionStorage中,再重新进入页面的时候,去取对应的sessionStorage值,判断是否存在,如果存在的话,立马通知对方断开连接
4.使用navigator.userAgent信息判断 ipad 浏览器的问题
ipad的浏览器使用 navigator.userAgent 进行判断的话,会发现返回的结果都是 safire,其实是因为ipad上的浏览器本质都是safire,只是套上了它们各自的壳而已,所以这边我们可以借助插件的力量 clientjs
不过这个插件目前依旧 判断不了ipad上的火狐浏览器,返回的值依旧是safire,这个要注意一下
5.关于navigator.mediaDevices.getDisplayMedia()的兼容
(1)Chrome 和 Edge 浏览器可以直接调用。
火狐 和 Safari 需要一个用户手势(比如点击按钮)才能调用,否则会报 InvalidStateError 错误。
(2)火狐 浏览器如果在调用getDisplayMedia()后,点击拒绝会触发报错NotAllowedError:The request is not allowed by the user agent or the platform in the current context,此时需要刷新浏览器后才可以再次调用。
(3)safari 浏览器如果在调用 getDisplayMedia()后,点击此网站永不信任后,必须前往网站的偏好设置里面,去把屏幕共享状态改成请求,然后重启浏览器才可以重新调用。
如果点击不允许,次数在三次以内(包括三次),都可以直接重新再调用,但是次数一旦超过三次,必须重启浏览器才可以重新再调用。
以上safari情况错误信息都为:NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.
五、可能有用的 scss 上的兼容处理
1.关于hover的兼容
问题表现: 因为移动设备上是没有hover效果的,当我们按钮上设置了hover效果后,有些机型第一次点击时会显示hover效果,第二次点击时才会正确反应。
解决方案: 判断是否移动设备,在最外层祖先元素上增加class(例:mod-pc),然后利用 @mixin、@include 进行解决
这段可全局引入
@mixin hover {
.mod-pc &:hover{
@content;
}
}
<template>
<div class="parent" :class="{'mod-pc': !isMobile}">
<button>我需要hover效果</button>
</div>
</template>
<style scoped lang="scss" type="text/scss">
.parent{
@include hover{
cursor: pointer;
background-color: #F6F6F6;
}
}
</style>
2.关于 elementUI 的样式兼容问题
elementUI 在移动端上,如果进行设备横竖屏旋转导致重绘,元素的font-size可能会出现设置无效的情况 类似于以下,内容和元素穿插的情况
<div>
内容内容
<b>内容内容</b>
内容内容
</div>
这时要注意给div元素设置 display: inline-block;
写在最后
如果文中有写错的地方,希望大家可以帮忙指正一下,一起学习,一起进步!