实时音频播放

需求

还是上个学校的项目,需要实时播放音频。

拾音器有一个rtsp的地址,然后去找了很多插件,前端可以直接解析用的,尝试了很多方法,ff什么的都没有实现,可能是没有找对方法,我觉得再这么下去肯定不能按期交付了,所以和后台沟通了一下,还是通过websocket传输二进制,前端拿到后再解析,然后去播放。

ok,有思路了就开始实现。

实现步骤

  1. websocket连接,这里websocket不做过多解释,具体可看上篇文章。
// 因为需求是需要点击之后开始播放,所以需要在useEffect中写,而是在点击事件里。
 const playAudio = async (data: any) => {
 const ws2 = new WebSocket(url2);
 setAudioSocket(() => ws2)
 }
  1. 定义相关回调函数
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!
希望能帮助到你们。如果大家有更好的办法,可以交流,或者指正。

作者:Suo不倒原文地址:https://segmentfault.com/a/1190000043843656

%s 个评论

要回复文章请先登录注册