实时音频播放
需求
还是上个学校的项目,需要实时播放音频。
拾音器有一个rtsp的地址,然后去找了很多插件,前端可以直接解析用的,尝试了很多方法,ff什么的都没有实现,可能是没有找对方法,我觉得再这么下去肯定不能按期交付了,所以和后台沟通了一下,还是通过websocket传输二进制,前端拿到后再解析,然后去播放。
ok,有思路了就开始实现。
实现步骤
- websocket连接,这里websocket不做过多解释,具体可看上篇文章。
// 因为需求是需要点击之后开始播放,所以需要在useEffect中写,而是在点击事件里。
const playAudio = async (data: any) => {
const ws2 = new WebSocket(url2);
setAudioSocket(() => ws2)
}
- 定义相关回调函数
useEffect(() => {
if (audioSocket) {
audioSocket.binaryType = 'blob';
audioSocket.onopen = () => {
console.log('音频链接上拉!!')
};
audioSocket.onmessage = (data: any) => audioAcceptMessage(data);
audioSocket.onclose = () => audioWebSocketOnClose();
audioSocket.onerror = () => audioWebSocketOnError();
return () => {
audioSocket.close();
receivedBlobs = []
}
}
}, [audioSocket])
3.处理音频数据,当时有尝试过各种缓存的api方法,但是都不理想。所以尝试了其他方式
// 该链接后台返回的是blob对象,而且是不间断的一直传送,每次解析出来的音频只有0.05秒左右,所以需要考虑做累加。
const audioAcceptMessage = async ({ data }: any) => {
if (data) {
console.log(data, '数据正常传输中')
// receivedBlobs为全局定义的数组,用来存储后台的数据
receivedBlobs.push(data)
} else {
// 无接收数据
}
}
4.实现方式1,手动缓存,并处理播放。
const renderAudio = async () => {
if (receivedBlobs.length === 0) return false;
// 如果数组的缓存过小,播放时间会太短,所以这里做了2秒延迟。
// 注:后台是不间断的一直传输二进制数据的。
if (receivedBlobs.length < 10) {
setAudioSrcLoading(true);
setTimeout(() => {
renderAudio();
}, 2000)
return false;
}
const blob = new Blob([...receivedBlobs], { type: 'audio/mp3' });
receivedBlobs = [];
// 这个url已经可以被audio标签直接识别播放了
const url = URL.createObjectURL(blob);
setAudioSrc(url)
// 以下步骤主要为了获取到这段音频的播放时长,以供触发下一个定时器
const temporaryAudio = new Audio();
temporaryAudio.src = url;
temporaryAudio.addEventListener('loadedmetadata', () => {
let time = temporaryAudio.duration * 1000;
audioTimeout = setTimeout(() => {
renderAudio();
}, time)
});
}
<audio src={audioSrc} autoPlay></audio>
缺点:每播放几秒钟,就会缓冲一下(数据处理时间和赋值url时间)。好吧,体验不过关。但是确实实现了。
5.实现方式2:想到了一个偏方,如果1个播放器会卡顿,那两个呢,两个做衔接播放呢,有想法了,开干。
// 实时音频播放地址
const [audioSrc, setAudioSrc] = useState<any>();
// 实时音频播放地址2
const [audioSrc2, setAudioSrc2] = useState<any>();
// 实时音频是否缓冲
const [audioSrcLoading, setAudioSrcLoading] = useState<boolean>(false);
const renderAudio = async () => {
if (receivedBlobs.length === 0) return false;
if (receivedBlobs.length < 10) {
setAudioSrcLoading(true);
setTimeout(() => {
renderAudio();
}, 2000)
return false;
}
const blob = new Blob([...receivedBlobs], { type: 'audio/mp3' });
receivedBlobs = [];
const url = URL.createObjectURL(blob);
// 这里用开关区分,下一次赋值给哪一个url
isAudioSrcFlag ? setAudioSrc(url) : setAudioSrc2(url);
isAudioSrcFlag = !isAudioSrcFlag;
const temporaryAudio = new Audio();
temporaryAudio.src = url;
temporaryAudio.addEventListener('loadedmetadata', () => {
let time = temporaryAudio.duration * 1000;
// 这里在下一段结束之前 提前播放,如果不提前播放,那还是会和第一种方式一样,没有改变。
const connectTime = 200; // 衔接播放提前时长
time = time > connectTime ? time - connectTime : time;
audioTimeout = setTimeout(() => {
renderAudio();
}, time)
console.log(`---------此段音频时长为:${temporaryAudio.duration} 秒,下段${time / 1000} 秒后播放}`);
});
}
<audio src={audioSrc} autoPlay></audio>
<audio src={audioSrc2} autoPlay></audio>
略微改造,功能实现了,嗯.... 还是有个问题,因为这200毫秒是经过测试以后,最不卡顿的临界值,所以每个下一次的播放都会提前,也就会导致缓存数据越来越少,在经过一段时间之后,还是需要缓冲一下,才能继续播放,还是没达到理想状态。好吧,看来这种方式不太能达到自己想要的。
那就继续优化!
6.实现方式3:这次采用了api播放音频,不用audio标签了。
const renderAudio = async () => {
if (receivedBlobs.length === 0) return false;
if (receivedBlobs.length < 10) {
setAudioSrcLoading(true);
setTimeout(() => {
renderAudio();
}, 2000)
return false;
}
const fileReader = new FileReader()
fileReader.onload = () => {
const arrayBuffer = fileReader.result as ArrayBuffer;
audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start();
});
}
setAudioSrcLoading(false);
const blob = new Blob([...receivedBlobs], { type: 'audio/mp3' });
fileReader.readAsArrayBuffer(blob)
receivedBlobs = [];
const url = URL.createObjectURL(blob);
const temporaryAudio = new Audio();
temporaryAudio.src = url;
temporaryAudio.addEventListener('loadedmetadata', () => {
let time = temporaryAudio.duration * 1000;
// 采用这种方式做衔接播放,50毫秒就可以做到不卡顿,应该是省去了audio标签处理的时间
const connectTime = 50; // 衔接播放提前时长
time = time > connectTime ? time - connectTime : time;
audioTimeout = setTimeout(() => {
renderAudio();
}, time)
console.log(`---------此段音频时长为:${temporaryAudio.duration} 秒,下段${time / 1000} 秒后播放}`);
});
}
结论:功能实现,虽然也需要缓冲,但是做到了大家都能接受的范围,很长很长一段时间后才会缓冲一次,缓冲的间隔很长,所以也还算成功吧。
over!
希望能帮助到你们。如果大家有更好的办法,可以交流,或者指正。