1、需求:js调用麦克风做实时(应该是吧)对讲
js获取麦克风权限,需要浏览器支持 AudioContext。
AudioContext 从chrome35开始就完全支持。
在开始说话之前,需要用户允许浏览器获取麦克风权限。即:
if (window.navigator.mediaDevices) {
window.navigator.mediaDevices
// 获取浏览器麦克风权限
.getUserMedia({ 'audio': true })
// 用户同意赋予麦克风权限
.then(this.initRecordMicro)
// 用户拒绝麦克风权限,或者当前浏览器不支持
.catch(e => {
switch (e.message || e.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
this.$message.error('用户拒绝提供权限')
break
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
this.$message.error('浏览器不支持您当前选择的设备')
break
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
this.$message.error('无法发现指定的硬件设备')
break
default:
this.$message.error(`无法打开麦克风,原因:${e.code || e.name}`)
}
})
} else {
this.$message.error('您当前浏览器或者协议暂不支持麦克风')
}
如果用户允许了,那么就开始后续操作,不允许的话,那就没了。
然后就在initRecordMicro
里开始获取"流",.then
回调会传入麦克风返回的流数据。然后就
this.ctxAudio = new window.AudioContext()
this.sourceAudio = this.ctxAudio.createMediaStreamSource(this.streamAudio)
2、完整代码如下:
<div class="intercomMicroVol">
<div class="intercomMicroVolCtx" />
</div>
<div class="microPhone"
@mousedown.prevent="microPhoneMousedown"
@mouseup.prevent="microPhoneMouseup"
>
<el-icon />
</div>
使用的是vue+TS(很多地方暂未找到合适的type,就用了any。。),长按的时候开始,松开就结束。所以使用的是@mousedown.prevent
和@mouseup.prevent
export default class extends Vue{
private streamAudio: any
private ctxAudio:any
private sourceAudio:any
private maxVol=0
private scriptProcessor:any
private ws:any
// 做了一个声音大小的柱子
@Watch('maxVol')
private getVolStyle(val:any) {
const dom = document.querySelector('.intercomMicroVolCtx') as HTMLElement
if (val > 0) {
dom.style.height = `${val * 2.6 + 10}px`
} else {
dom.style.height = '0'
}
}
private mounted() {
// 加个事件,用以处理,处于对讲期间,用户误操作了F5等刷新(咱也不知道为啥会这么干,但是提了。。。就得加。。。)
window.addEventListener('beforeunload', (e) => this.beforeunloadHandler(e))
}
private destroyed() {
this.intercomMouseup()
window.removeEventListener('beforeunload', (e) => this.beforeunloadHandler(e))
}
private beforeunloadHandler(e: any) {
this.intercomMouseup()
}
private intercomMousedown() {
// 用来判断是否1秒内连续点击,处理并拦截
if (this.last && nowTime - this.last < 1000) {
// 用来判断是否已经有 提示
if (document.querySelectorAll('.el-message').length === 0) {
this.$message.warning('点的太快了,请稍后再点击~')
}
}else{
this.ws = new WebSocket('ws://ip:port')
this.ws.onopen = (e:any) => {
console.log('连接建立', e)
// 和后端约定好要传的东西
this.ws.send('someID')
this.startRecord()
}
this.ws.onerror = (e:any) => {
console.log(e)
}
}
}
private intercomMouseup() {
this.stopRecord()
}
// 当用户鼠标移出了对讲按钮,停止对讲
private intercomMouseleave() {
if (this.ws || this.sourceAudio) {
this.intercomMouseup()
}
}
private startRecord() {
// 这里没有使用上面的完整权限判断,因为这里是弹层操作。弹层之前先获取判断了。
if (window.navigator.mediaDevices) {
window.navigator.mediaDevices
// 获取浏览器麦克风权限
.getUserMedia({ 'audio': true })
// 用户同意赋予麦克风权限
.then(this.initRecordMicro)
// 用户拒绝麦克风权限,或者当前浏览器不支持
.catch(e => {
this.$message.error(`获取麦克风权限失败,原因:${e}`)
})
} else {
this.$message.error('您当前浏览器或者浏览器版本暂不支持麦克风')
}
}
private stopRecord() {
//关闭全部
const tracks = this.streamAudio.getAudioTracks()
for (let i = 0, len = tracks.length; i < len; i++) {
tracks[i].stop()
}
//把init里建立的audio链接都关闭
this.sourceAudio.disconnect()
this.scriptProcessor.disconnect()
this.sourceAudio = null
this.scriptProcessor = null
this.maxVol = 0
this.ws.close()
}
private initRecordMicro(stream:any) {
this.streamAudio = stream
this.ctxAudio = new window.AudioContext()
this.sourceAudio = this.ctxAudio.createMediaStreamSource(this.streamAudio)
// 通过 AudioContext 获取麦克风中音频音量
// 256, 512, 1024, 2048, 4096, 8192, 16384
// 默认支持2的整数次幂的数字,数字越大越保熟,低了容易延迟
this.scriptProcessor = this.ctxAudio.createScriptProcessor(4096, 1, 1)
this.sourceAudio.connect(this.scriptProcessor)
this.scriptProcessor.connect(this.ctxAudio.destination)
this.scriptProcessor.onaudioprocess = (audioProcessingEvent:any) => {
// buffer处理
// 只处理了单声道
const buffer = audioProcessingEvent.inputBuffer.getChannelData(0)
let sum = 0
let outputData:any = []
for (let i = 0; i < buffer.length; i++) {
sum += buffer[i] * buffer[i]
}
// 这里只是为了取数字,来展示声音的柱子
this.maxVol = Math.round(Math.sqrt(sum / buffer.length) * 100)
// 浏览器麦克风采样率为 this.ctxAudio.sampleRate 一般是44100
const inputSampleRate = this.ctxAudio.sampleRate
// 跟流对接,他们那边需要我提供的是8000采样率的,所以需要压缩一次
outputData = this.compress(buffer, inputSampleRate, 8000)
this.ws.send(outputData)
}
}
private floatTo16BitPCM(bytes:any) {
let offset = 0
const dataLen = bytes.length
// 默认采样率以16计算,而不是8位
const buffer = new ArrayBuffer(dataLen * 2)
const data = new DataView(buffer)
for (let i = 0; i < bytes.length; i++, offset += 2) {
// 保证采样帧的值在-1到1之间
let s = Math.max(-1, Math.min(1, bytes[i]))
// 将32位浮点映射为16位整形 值
// 16位的划分的是 2^16=65536,范围是-32768到32767
// 获取到的数据范围是[-1,1]之间,所以要转成16位的话,需要负数*32768,正数*32767,就可以得到[-32768,32767]范围内的数据
// 第三个参数,true 含义是 是否是小端字节序 这里设置为true
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
}
return data
}
private compress(data:any, inputSampleRate:number, outputSampleRate:number) {
const rate = inputSampleRate / outputSampleRate
const compression = Math.max(rate, 1)
const length = Math.floor(data.length / rate)
const result = new Float32Array(length)
let index = 0
let j = 0
while (index < length) {
// 取整
let temp = Math.floor(j)
result[index] = data[temp]
index++
j += compression
}
// 将压缩过的数据转成pcm格式数据
return this.floatTo16BitPCM(result)
}
}
3、补充个知识点
之前都是本地run的服务,localhost
访问是没问题的,但是部署之后发现无法获取window.navigator.mediaDevices
,是因为浏览器有保护机制,即,window.navigator.mediaDevices
只能在localhost
或者https
和file
中才可以。所以,记得https
啊~~~
打完收工