原理介绍:

项目需求 Web端播放实时音频流,折腾了两天后问题得以解决。记录下开发调试过程,方便后来者。首次想到是利用Audio标签,Audio标签可以直接播放MP3格式,服务端将实时音频流编码成WAV格式通过Http方式传给Web端即可。采用Audio Web API方式播放实时流会出现卡顿现象,以上方法一次性解码的数据可以连续播放,每次解码后要重新创建BufferSource,显而易见这种播放模式播放实时流效率很低,查阅了Audio Web API 文档 播放网络流似乎要利用,基于AudioWorkletProcessor的自定义节点,文档也给了一个简单的例子。另外一种可行的方法是服务端输出Rtmp音频流 通过video.js播放实际应该是用了flash。项目开始已经想到了这个方案可行只是觉得有些绕(需要将音频流封装后推送到rtmp server)。另外一个可行的方案就是利用H5 MSE实现,这个方案也有开源的库可以用,例如video.js 或flv.js这需要服务端将音频打包成flv格式,推送给Web前端,Web端接收到音频数据后调用flv.js进行播放。

前端代码如下所示:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Web Audio API 测试</title>
<script src="../static/js/reconnectingwebsocket.js"th:href=@{/js/reconnectingwebsocket.js}></script>
</head>
<body><table><tr><td>参数:</td><td><input style="width: 390px;" type="text" name="type"id="type"></td><td><button onclick="onSendMessage()">发送消息</button></td><td><button onclick="onCloseMessage()">断开连接</button></td></tr></table>
</body><script>//Web Audio APIvar nextStartTime = 0;var context = null;try {context = new (window.AudioContext || window.webkitAudioContext)();} catch (e) {alert('您当前的浏览器不支持Web Audio API ');}var objSocket = null;var wsUrl = 'ws://localhost:8080/Audiowebsocket';if ('WebSocket' in window) {//objSocket = new ReconnectingWebSocket(wsUrl);objSocket = new WebSocket(wsUrl);} else {alert('Not support websocket')}/** WebSocket服务连接*/objSocket.onopen = function(evt) {onOpen(evt)};objSocket.onclose = function(evt) {onClose(evt)};objSocket.onmessage = function(evt) {onMessage(evt)};objSocket.onerror = function(evt) {onError(evt)};function onOpen(evt) {console.log("Connected to WebSocket server.");}function onClose(evt) {console.log("Disconnected");}function onError(evt) {console.log('Error occured: ' + evt.data);}function onMessage(evt) { //websocket返回数据信息处理//console.log('Retrieved data from server: ' + evt.data);var reader = new FileReader(); //文件阅读器reader.readAsArrayBuffer(evt.data); //读取成ArrayBuffer对象reader.onload = function() { //读取完毕//解码context.decodeAudioData(this.result, function(buffer) {console.log("decode success");playSound(buffer);}, function(e) {console.log("decode failed");"Error with decoding audio data" + e.err});}//--------onMessage-----end-----------}//播放声音function playSound(buffer) {var source = context.createBufferSource();source.buffer = buffer;if (nextStartTime == 0) {nextStartTime = context.currentTime + buffer.duration / 2;}source.connect(context.destination);console.log("nextStartTime=" + nextStartTime);source.start(nextStartTime);//source.start(0);nextStartTime += buffer.duration;if (nextStartTime > 11) {nextStartTime = 0;}}//发送消息function onSendMessage() {var mess = document.getElementById("type").value;objSocket.send(mess);/* if (window.confirm("发送消息:" + mess)) {//alert("确定");objSocket.send(mess);return true;} else {//alert("取消");return false;} */}//关闭连接function onCloseMessage() {objSocket.close()}
</script>
</html>

后端核心代码:

@Controller
@RequestMapping("/audio")
public class AudioController {@RequestMapping("/a1")public String websocketAduo() {return "audio/WebAudio";}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.CopyOnWriteArraySet;import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
import org.springframework.stereotype.Component;import lombok.extern.slf4j.Slf4j;@Slf4j
@ServerEndpoint(value = "/Audiowebsocket")
@Component
public class AudioWavWebsocket {private static int onlineCount = 0;private static CopyOnWriteArraySet<AudioWavWebsocket> webSocketSet = new CopyOnWriteArraySet<AudioWavWebsocket>();// 与某个客户端的连接会话,需要通过它来给客户端发送数据private Session session;/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session) {this.session = session;webSocketSet.add(this); addOnlineCount(); File file = new File("./src/main/resources/mp3/05.wav");try {FileInputStream fis = new FileInputStream(file);int readLength = 1024;byte[] b = new byte[readLength];int n;int index = 0;byte[] header = WAVFile.getwriteWavFileHeader(file);int headLen = header.length;ByteBuffer buffer = ByteBuffer.allocate(readLength + headLen);while ((n = fis.read(b)) != -1) {index++;buffer.flip();buffer.clear();byte[] subHeader=ByteUtils.subArray(b, 0, 4);if (!isWAVFileHeader(subHeader)) {log.info("the WAV file body...");buffer.put(header, 0, headLen);buffer.put(b, 0, n);} else {log.info("the WAV file header...");buffer.put(b, 0, n);}buffer.flip();log.info("Send消息:{}-{}", index, n);sendMessage(buffer);}fis.close();} catch (IOException e) {log.error("IO异常");}}/*** 音频流判断* @author: tompai* @createTime: 2020年12月26日 下午11:17:21* @history:* @param header* @return boolean*/private boolean isWAVFileHeader(byte[] header) {// WAV文件开始标志//byte[] head = {'R','I','F','F'}; // RIFFreturn ByteUtils.equals(header, head);}/*** 连接关闭调用的方法*/@OnClosepublic void onClose() {webSocketSet.remove(this); // 从set中删除subOnlineCount(); // 在线数减1}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, Session session) {log.info("来自客户端的消息:" + message);// 群发消息for (AudioWavWebsocket item : webSocketSet) {try {item.sendMessage(message);} catch (IOException e) {e.printStackTrace();}}}/*** 发生错误时调用*/@OnErrorpublic void onError(Session session, Throwable error) {log.error("发生错误");error.printStackTrace();}public void sendMessage(String message) throws IOException {log.info("来自后端的消息:{}", message);this.session.getBasicRemote().sendText(message);}public void sendMessage(ByteBuffer buffer) throws IOException {// log.info("来自后端的消息:{}", buffer.);this.session.getBasicRemote().sendBinary(buffer);}/*** 群发自定义消息*/public static void sendInfo(String message) throws IOException {for (AudioWavWebsocket item : webSocketSet) {try {item.sendMessage(message);} catch (IOException e) {log.error("发生错误");continue;}}}public static synchronized int getOnlineCount() {return onlineCount;}public static synchronized void addOnlineCount() {AudioWavWebsocket.onlineCount++;}public static synchronized void subOnlineCount() {AudioWavWebsocket.onlineCount--;}public Session getSession() {return session;}public void setSession(Session session) {this.session = session;}public static CopyOnWriteArraySet<AudioWavWebsocket> getWebSocketSet() {return webSocketSet;}public static void setWebSocketSet(CopyOnWriteArraySet<AudioWavWebsocket> webSocketSet) {AudioWavWebsocket.webSocketSet = webSocketSet;}
}
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;public class WAVFile {/*** @author: tompai* @createTime: 2020年12月22日 下午11:57:26* @history:* @param args void*/public static void main(String[] args) {// TODO Auto-generated method stubFile f = new File("./src/main/resources/mp3/02.wav");try {RandomAccessFile rdf = new RandomAccessFile(f, "r");System.out.println("声音尺寸: " + toInt(read(rdf, 4, 4))); // 声音尺寸System.out.println("音频格式: " + toShort(read(rdf, 20, 2))); // 音频格式 1 = PCMSystem.out.println("声道数: " + toShort(read(rdf, 22, 2))); // 1 单声道 2 双声道System.out.println("采样率: " + toInt(read(rdf, 24, 4))); // 采样率、音频采样级别 8000 = 8KHzSystem.out.println("波形的数据量: " + toInt(read(rdf, 28, 4))); // 每秒波形的数据量System.out.println("采样帧: " + toShort(read(rdf, 32, 2))); // 采样帧的大小System.out.println("采样位数: " + toShort(read(rdf, 34, 2))); // 采样位数rdf.close();} catch (FileNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}public static int toInt(byte[] b) {//return ((b[3] << 24) + (b[2] << 16) + (b[1] << 8) + (b[0] << 0));return (((b[3] & 0xff) << 24) + ((b[2] & 0xff) << 16) + ((b[1] & 0xff) << 8) + ((b[0] & 0xff) << 0));}public static short toShort(byte[] b) {return (short) ((b[1] << 8) + (b[0] << 0));}public static byte[] read(RandomAccessFile rdf, int pos, int length) throws IOException {rdf.seek(pos);byte result[] = new byte[length];for (int i = 0; i < length; i++) {result[i] = rdf.readByte();}return result;}public static byte[] getwriteWavFileHeader(File wavFile) {try {RandomAccessFile rdf = new RandomAccessFile(wavFile, "r");long totalAudioLen = toInt(read(rdf, 4, 4));long longSampleRate = toInt(read(rdf, 24, 4));int channels = toShort(read(rdf, 20, 2));int audioFormat = toShort(read(rdf, 34, 2));rdf.close();return writeWavFileHeader(totalAudioLen, longSampleRate, channels, audioFormat);} catch (FileNotFoundException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}return null;}/*** @param totalAudioLen  不包括header的音频数据总长度* @param longSampleRate 采样率,也就是录制时使用的频率、音频采样级别 8000 = 8KHz* @param channels       audioRecord的声道数1/2* @param audioFormat    采样精度; 譬如 16bit* @throws IOException 写文件错误*/private static byte[] writeWavFileHeader(long totalAudioLen, long longSampleRate, int channels, int audioFormat)throws IOException {byte[] header = generateWavFileHeader(totalAudioLen, longSampleRate, channels, audioFormat);return header;}/*** @param totalAudioLen  不包括header的音频数据总长度* @param longSampleRate 采样率,也就是录制时使用的频率* @param channels       audioRecord的频道数量* @param audioFormat    采样精度; 譬如 16bit*/private static byte[] generateWavFileHeader(long totalAudioLen, long longSampleRate, int channels,int audioFormat) {long totalDataLen = totalAudioLen + 36;long byteRate = longSampleRate * 2 * channels;byte[] header = new byte[44];header[0] = 'R'; // RIFFheader[1] = 'I';header[2] = 'F';header[3] = 'F';// 文件长度 4字节文件长度,这个长度不包括"RIFF"标志(4字节)和文件长度本身所占字节(4字节),即该长度等于整个文件长度 - 8header[4] = (byte) (totalDataLen & 0xff);header[5] = (byte) ((totalDataLen >> 8) & 0xff);header[6] = (byte) ((totalDataLen >> 16) & 0xff);header[7] = (byte) ((totalDataLen >> 24) & 0xff);// fcc type:4字节 "WAVE" 类型块标识, 大写header[8] = 'W';header[9] = 'A';header[10] = 'V';header[11] = 'E';// FMT Chunk 4字节 表示"fmt" chunk的开始,此块中包括文件内部格式信息,小写, 最后一个字符是空格header[12] = 'f'; // 'fmt 'header[13] = 'm';header[14] = 't';header[15] = ' ';// 过渡字节// 数据大小 4字节,文件内部格式信息数据的大小,过滤字节(一般为00000010H)header[16] = 16;header[17] = 0;header[18] = 0;header[19] = 0;// 编码方式 10H为PCM编码格式 FormatTag:2字节,音频数据的编码方式,1:表示是PCM 编码header[20] = 1; // format = 1header[21] = 0;// 通道数 Channels:2字节,声道数,单声道为1,双声道为2header[22] = (byte) channels;header[23] = 0;// 采样率,每个通道的播放速度header[24] = (byte) (longSampleRate & 0xff);header[25] = (byte) ((longSampleRate >> 8) & 0xff);header[26] = (byte) ((longSampleRate >> 16) & 0xff);header[27] = (byte) ((longSampleRate >> 24) & 0xff);// 音频数据传送速率,采样率*通道数*采样深度/8// 4字节,音频数据传送速率, 单位是字节。其值为采样率×每次采样大小。播放软件利用此值可以估计缓冲区的大小// byteRate = sampleRate * (bitsPerSample / 8) * channelsheader[28] = (byte) (byteRate & 0xff);header[29] = (byte) ((byteRate >> 8) & 0xff);header[30] = (byte) ((byteRate >> 16) & 0xff);header[31] = (byte) ((byteRate >> 24) & 0xff);// 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数header[32] = (byte) (2 * channels);header[33] = 0;// 每个样本的数据位数// 2字节,每个声道的采样精度; 譬如 16bit 在这里的值就是16。如果有多个声道,则每个声道的采样精度大小都一样的;header[34] = (byte) audioFormat;header[35] = 0;// Data chunk// ckid:4字节,数据标志符(data),表示 "data" chunk的开始。此块中包含音频数据,小写;header[36] = 'd';header[37] = 'a';header[38] = 't';header[39] = 'a';// 音频数据的长度,4字节,audioDataLen = totalDataLen - 36 = fileLenIncludeHeader - 44header[40] = (byte) (totalAudioLen & 0xff);header[41] = (byte) ((totalAudioLen >> 8) & 0xff);header[42] = (byte) ((totalAudioLen >> 16) & 0xff);header[43] = (byte) ((totalAudioLen >> 24) & 0xff);return header;}
}

基于WebSocket实现的前端实时声音告警提醒功能相关推荐

  1. 基于 websocket 实现的 im 实时通讯案例

    分享利用 redis 订阅与发布特性,巧妙的现实高性能im系统.为表诚意,先贴源码地址:https://github.com/2881099/im 下载源码后的运行方法: 运行环境:.NETCore ...

  2. 基于微信小程序的图书借阅到期提醒功能实现+后台管理系统(SSM+mysql)-JAVA.VUE【数据库设计、论文、源码、开题报告】

    功能介绍 管理员功能: (1)用户登录注册模块:所有用户在使用本系统前都要进行登录操作,登录成功后才能进入系统做其它操作.如果用户还没有登录账号就先注册一下然后再登录. (2)图书馆借阅到期提醒信息列 ...

  3. 基于netty+websocket实现门户游客实时统计功能

    基于netty+websocket实现门户游客实时统计功能 基本需求 商城门户页面需要实时展示游客访问的数量,商城后台页面需要实时游客访问量.登录用户数量,以及下订单用户数量. 技术选型 1.首先实时 ...

  4. goahead处理json_基于websocket和goahead实现前端RTSP流视频控制

    最近在开发嵌入式设备视频监控的前端程序,前端使用vue框架实现基本逻辑,后端是C++,用webpack编译器编译成asp过程,服务器是goahead,用goahead解析asp过程,通过websock ...

  5. 基于PyQt5的YOLOv5检测界面——YOLOv5检测目标后声音告警的美化

    之前的博文写了如何使YOLOv5在检测到目标后进行声音告警提示,本次的博文是对其进行界面上的优化,让其更加方便易用,距离产品更近了一步. 主要参考了这位大佬的代码,并根据自己的需要,进行了添入语音告警 ...

  6. python websocket django vue_Django资料 Vue实现网页前端实时反馈输出信息

    Django资料 Vue实现网页前端实时反馈输出信息 前言 功能实现:网也点击任务,页面实时返回执行的信息 本次的任务是执行本地的一个sh脚本 这个sh脚本就是每隔1S,输出一段文字 如果需要远程可以 ...

  7. 基于websocket的网页实时消息推送与在线聊天(上篇)

    文章目录 @[toc] 基于websocket的网页实时消息推送与在线聊天(上篇) "使用dwebsocket在django中实现websocket" websocket原理图 d ...

  8. 基于Flink的电影数据实时统计平台(三):前端构建

    文章目录 一.环境准备 二.前端构建 2.1 页面绘制 2.1.1 网站框架`App.vue` 2.1.2 前台观影/电影首页`Movie.vue` 2.1.3 前台观影/播放电影`Player.vu ...

  9. 大数据主题分享第三期 | 基于ELK的亿级实时日志分析平台实践

    猫友会希望建立更多高质量垂直细分社群,本次是"大数据学习交流付费群"的第三期分享. "大数据学习交流付费群"由猫友会联合,斗鱼数据平台总监吴瑞诚,卷皮BI技术总 ...

  10. ASP.NET Core2基于RabbitMQ对Web前端实现推送功能

    在我们很多的Web应用中会遇到需要从后端将指定的数据或消息实时推送到前端,通常的做法是前端写个脚本定时到后端获取,或者借助WebSocket技术实现前后端实时通讯.因定时刷新的方法弊端很多(已不再采用 ...

最新文章

  1. VR规格表出来啦!Rift,Vive,PSVR,Acer和HP
  2. 关于Operation 的那些事
  3. 如何实现在O(n)时间内排序,并且空间复杂度为O(1)
  4. Qt下Tcp传输文件
  5. synchronized 底层如何实现?什么是锁升级、降级?
  6. LeetCode 1067. 范围内的数字计数
  7. linux闹钟软件下载,电量充满警示闹铃
  8. 排序方法总结C++实现
  9. python3安装cx oracle,[求助]python3安装cx_Oracle 报错
  10. hadoop配置文件还原_hadoop配置文件详解,安装及相关操作
  11. 视觉控每天盯着桌面,少不了桌面手机壁纸图片,请收好
  12. makefile:425: *** 遗漏分隔符
  13. 电话号码被标记,快速取消教程
  14. Firefox浏览器无法安装firebug和firepath插件问题解决
  15. 数据绑定之DataFormatString
  16. arcgis重分类读不出值的解决办法?
  17. android页面监听扫描枪,GitHub - rorine/BarcodeScannerGunMaster: android 扫码枪 内容读取,与拦截editText消费...
  18. 高数__已知2个平面方程, 求这2个平面的夹角
  19. 图像分类经典卷积神经网络—SENet论文翻译(中英文对照版)—Squeeze-and-Excitation Networks(挤压和激励网络)
  20. 本地获取jqdata的港股通资金数据保存为sql数据库

热门文章

  1. 22.Proxy Objects
  2. 17. Element dir 属性
  3. css3中旋转坐标轴的问题
  4. [2019杭电多校第六场][hdu6641]TDL
  5. java 二叉树的创建 遍历
  6. Ajax json 数据格式
  7. 第一篇:webservice初探
  8. POJ 3264:Balanced Lineup(RMQ模板题)
  9. 运算符重载,输出流运算符重载
  10. Android widget开发有感