作 者
受音视频毒打的萌新,此生不会再爱了。
播放场景
如果是socket.io的话,如下:
const socket = io('yourServerAddress');
socket.on('yourEvent', audioFrameData => {
// 播放业务逻辑代码
})
如果用的是websocket的话,如下:
const ws = new WebSocket('yourServerAddrsss');
ws.onmessage = (event) => {
// 播放业务逻辑代码
}
不管是socket.io还是websocket,只是传输协议的不同,播放逻辑都是一样的。
下面的播放代码都有socket.io做样例,不再一一说明。
方法一
网上搜到的大多是这种,通过AudioContext解码音频帧数据,然后创建BufferSource去逐帧播放。
// 创建 AudioContext 实例
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 待播放的chunks
let audioBufferChunks = [];
// 控制是否开始播放
let audioStarted = false;
const socket = io('yourServerAddress');
socket.on('yourEvent', audioFrameData => {
// 播放业务逻辑代码
// 转成arrayBuffer
const arrayBufferMeta = stringToArrayBuffer(audioFrameData, 'hex');
// 解码
audioContext.decodeAudioData(arrayBufferMeta.buffer, (buffer) => {
audioBufferChunks.push(buffer)
// 如果还没有开始播放,触发第一次播放
if (audioStarted === false) {
playSound()
}
})
})
// 播放帧
function playSound() {
try {
if (audioBufferChunks.length !== 0) {
audioStarted = true;
// 创建当前播放音频帧要用的bufferSource
const source = audioContext.createBufferSource();
// 把chunks数组里的第一帧放入source里
source.buffer = audioBufferChunks[0]
source.connect(audioContext.destination)
// 开始播放此帧
source.start(0)
// 监听结束事件
source.addEventListener('ended', () => {
// 去掉已播放完的帧
audioBufferChunks.splice(0, 1);
// 回调继续播放
playSound();
})
} else {
audioStarted = false;
}
} catch (e) {
console.log(e)
audioStarted = false;
}
}
// 传输的时候,服务器端可能会转成base64字符串传输,也可能转成hex字符串(十六进制)传输
function stringToArrayBuffer(string, type) {
// 无论怎样,要先把数据转成js里的ArrayBuffer
if (type === 'base64') {
// 代码找不到了 =.=
}
if (type === 'hex') {
// remove the leading 0x
const hexString = string.replace(/^0x/, '');
// ensure even number of characters
if (hexString.length % 2 != 0) {
console.log('WARNING: expecting an even number of characters in the hexString');
}
// check for some non-hex characters
const bad = hexString.match(/[G-Z\s]/i);
if (bad) {
console.log('WARNING: found non-hex characters', bad);
}
// split the string into pairs of octets
const pairs = hexString.match(/[\dA-F]{2}/gi);
// convert the octets to integers
const integers = pairs.map(function (s) {
return parseInt(s, 16);
});
const array = new Uint8Array(integers);
// console.log(array);
return array;
}
}
这个方法可用,但是有一个很严重的问题,帧与帧之间切换的时候,会有爆音,非常明显,根本不能用于生产环境。
当时试着在帧与帧之间加淡入淡出效果,一定程序上缓解了爆音的问题,但随之而来的是播放过程的声音间接性的糊一下糊一下的。
帧与帧之间淡入淡出效果实现如下:
/**
* 前面省略
*/
const source = audioContext.createBufferSource();
const currBuffer = audioBufferChunks[0];
let currChannel = currBuffer.getChannelData(0);
// 开始和结尾的1.5秒做淡入淡出,以下以48000的采样率算
const sd = 48000 / 1000 * 1.5;
const ed = currChannel.lenght - sd;
for (let i=0; i < currChannel.lenght; i++) {
let factor = 1;
if (i < sd) {
factor = i / sd;
} else if (i > ed) {
factor = (currChannel.lenght - 1) / sd
}
// i的值会在0~1之间
// 乘以1还是它自己,剩以0则为0,可以想像为这个factor就是音量大小
currChannel[i] = currChannel[i] * factor
}
// 后续的都一样,把currBuffer赋给source.buffer
source.buffer = currBuffer;
/**
* 后面省略
*/
方法二
经过上面这两番折腾,老板还是不放过!爆音不能接受!声音忽高忽低也不能接受!直到无意间在MDN上看到这个「实验中的功能」,彻底拯救了我!
用法如下:
首先,写一个audio
标签,设为display:none;
不显示
<audio id="audioPlay" style="display:none !important" controls autoplay></audio>
js 部分
// 你那边要用的音频格式支不支持要去MDN上查一下
const mineCodec = 'audio/aac';
// 声明 SourceBuffer
let sourceBuffer;
// 浏览器兼容性检查
if ('MediaSource' in window && MediaSource.isTypeSupported(mineCodec)) {
// 声明MediaSource
const mediaSource = new MediaSource();
// 取得dom元素
const audioPlay = document.getElementById('audioPlay');
// 创建ObjectURL
audioPlay.src = window.URL.createObjectURL(mediaSource)
// 监听source打开的事件
mediaSource.addEventListener('sourceopen', () => {
// 给 sourceBuffer 赋值
sourceBuffer = mediaSource.addSourceBuffer(mineCodec)
})
} else {
console.error('unsupported MIME type or codec: ', mineCodec)
}
socket.on('yourEvent', (audioFrameData) => {
// 转成arrayBuffer
const arrayBufferMeta = stringToArrayBuffer(audioFrameData, 'hex');
// 就这样,非常简单
sourceBuffer.appendBuffer(arrayBufferMeta);
})
后记
后来,公司某个项目中用了国内某摄像头厂家的解决方案,发现他们的网页播放器用的也是方法二,算是大厂背书了吧。此坑已填好,从此我与音视频是路人了,不想再碰音视频!!!