我们在实际的开发过程会遇到很多的问题,这里总结和归纳,可以帮助各位
流式协议带来的限制
媒体流属性的随机化处理
- RTC 协议要求接收方在接收到媒体流后复写 media track 上的 id, label, contentHint 等属性以保证流属性不会泄漏发送者的媒体设备信息,并使流在 P2P 网络中唯一.
- 这导致接收者无法区分流中的同类型 track. 例如发送者将相机,屏幕共享两条视频轨道合在同一条流中发给接受者,发送者不知道这两条流到接受者处时 Id 分别是什么,接受者也无法区分哪个是屏幕共享,哪个是视频流
- 解决方法:在传输多个 track 需要分离的时建立多条 P2P 连接 (虽然看起来很蠢)
FireFox 禁止两个相同节点之间无法建立多条连接
- FirFox 禁止两个相同节点做多次媒体协,也就是说 A 节点最多 call B 节点一次.
- 解决方案:如果需要建立两条流 (例如相机和屏幕共享流) 就让 A, B 互相 call 一次
一旦连接建立,不能添加或者删除 Track
- 建立 RTC 连接的时候虽然我们传入的是一个流,但是协议会获取流中的所有 Track, 然后传输这些 Track, 这就意味着协议并不会引用流对象,我们为流加入或者删除 Track 无法对 RTC 产生影响.
- 例如用户目前正在使用前置摄像头,当用户切换到后置摄像头时,如果我们删除流中的前置媒体流,添加后置媒体流,RTC 连接并不会切换轨道,反而会认为无法获取轨道信息
- 解决方案:在建立连接时创建最大可能使用数量的占位流,在增减设备时使用 RTC API 中的 replaceTrack 方法替换占位流。例如:在传输摄像头和麦克风数据时,我们可能需要采集麦克风,摄像头,背景虚化遮罩的 Track, 那么我们就需要使用一个占位 AudioTrack, 两个占位 VideoTrack. 当启动摄像头时候,将占位 Video Track 切换为实际 Track, 在禁用摄像头时再切换回去
RTC 连接状态统计
- RTC 并不支持直接汇总全部 RTC 连接状态,只支持统计某一个连接的连接状态。开发者可以通过
RTCPeerConnection.getStats
获取对于一个连接的不同主题的报告。常见的有:- 统计比特率:
- 获取当前连接发出或者接收到的总字节数
- 求和所有连接的收发的总字节数
- 比特率 =
8 * 两次检测时的字节数差 / 两次检测间隔
- 获取收发字节数
- 获取下行字节数:
report.type === 'inbound-rtp'
的报告的report.bytesReceived
- 获取上行字节数:
report.type === 'outbound-rtp'
的报告的report.bytesReceived
- 获取下行字节数:
- 区分媒体类型
- 视频:
report.mediaType === 'video'
- 音频:
report.mediaType === 'audio'
- 视频:
- 统计下行丢包率:
- 一个连接的累计丢包:
report.type === 'inbound-rtp'
报告的report.packetsLost
- 一个连接的从收包:
report.type === 'inbound-rtp'
报告的report.packageReceived
- 总下行丢包率:
所有连接的丢包量求和 / (所有连接的丢包量求和 + 所有连接的收包量和)
- 网络延迟:
- 一个连接的延迟:
report.currentRoundTripTime
(单位: s) - 总延迟:
所有延迟求和 / 连接数量
- 视频帧率:
- 上行连接帧率:
report.type === 'outbound-rtp'
报告的report.framesPerSecond
- 下行连接帧率:
report.type === 'inbound-rtp'
报告的report.framesPerSecond
- 总帧率:
连接帧率求和 / 连接数
- 视频分辨率:
- 上行连接分辨率:
report.type === 'outbound-rtp'
报告的report.frameWidth
,report.frameHeight
- 下行连接分辨率:
report.type === 'inbound-rtp'
报告的report.frameWidth
,report.frameHeight
- 统计比特率:
- 实现可以参考 conflux-client/src/utils/report.ts at master · KairuiLiu/conflux-client · GitHub
- 用来调试的 RTC 连接状态报告: chrome://webrtc-internals/
媒体流的处理与兼容性问题
媒体权限的获取与浏览器兼容性问题
- 在首次
navigator.mediaDevices.getUserMedia()
浏览器会提示用户授权。但是这会导致设备列表只有在获取用户设备时才刷新。不方便 UI 展示。建议采用如下方式:- 判断是否授权:
- 现代化的方法:使用
navigator.permissions
- 但是该 API 存在兼容性问题,只有 Chrome 与 Safari 支持
microphone
与camera
这两个permissionName
, FireFox 会报错 - 兜底方法:检查
navigator.mediaDevices.enumerateDevices()
中某类 type 是否没有或者 Label 为空。如果是则证明要么用户没有这类设备,要么没有授权。该函数不会让浏览器弹出授权 - 总结:
- 现代化的方法:使用
- 申请授权:
- 监控授权状态变化
- 监控设备变化
- 判断是否授权:
指定音频输出设备
- 注意:仅桌面 Chrome 与 FireFox 浏览器支持支持,TS 也没有这个方法
// @ts-ignore audioElement?.setSinkId?.(speaker.deviceId);
指定音视频采集设备
const constraints: MediaStreamConstraints = {[type]: { deviceId: { exact: deviceId } },}; const stream = await navigator.mediaDevices.getUserMedia(constraints);
音频轨道音量统计
- 注意:音量取值是 [0,1], 当麦克风离得比较远时采集的音量一直很小,进度条展示出来一直处于低点。可以采用 softmax 对结果做变化,让用户可以明显看到音量变化
- 注意:
AudioContext
并不能指定音频播放设备,因此不能用它来顺便播放音频. - 原理:
- 创建
AnalyserNode
用于实时获取音频信号的时间域和频率域数据 (默认使用 FFT 采样) - 使用
getByteTimeDomainData
获取最近的时域数据,值代表振幅值 (取值 0-256, 128 为静音) - 每个样本值减去 128, 使得波形数据中心化到 0 (静音为 0)
- 将每个去中心化后的值求平方,得到每个样本的能量,计算所有样本能量的平均值
- 计算平均能量的平方根,得到均方根值,(当音量最大时,均方根为 255)
- 创建
如何解决ontrack事件不触发的问题?
1、确保正确绑定了事件,检查代码中的语法错误,并确保使用正确的轨道类型和流对象。
const localStream = await navigator.mediaDevices.getUserMedia({ video: true }); localStream.addEventListener('ontrack', (event) => { console.log('ontrack event triggered'); });
2、如果仍然无法触发事件,尝试使用不同的轨道类型或流对象进行测试。
Web 通话过程中出现回声、杂音、噪声、声音小?
通话双方的设备相距太近的时候,属于正常现象,测试时请相互距离远一点。当其他端听到 Web 端的声音存在回声、噪声、杂音等情况时,说明 Web 端的 3A 处理没有生效。
若使用了浏览器原生 getUserMedia API 进行自定义采集,则需要手动设置 3A 参数:
- echoCancellation:回声消除开关
- noiseSuppression:噪声抑制开关
- autoGainControl:自动增益开关? 详细设置参考 媒体追踪约束 。