Web对讲/广播功能
Web对讲/广播功能
- 介绍
介绍
轻量级的 Web 对讲/广播网关系统 。利用 Netty 的高性能网络处理能力,实现了从 Web 前端采集音频,经过服务端转码与协议封装,最终以标准 GB28181/RTP 协议推送到前端设备(如摄像机、NVR 或国标平台)的核心功能。
1. 核心功能模块
1.1. Web 接入与信令层 (Web Access Layer)
- 核心类 : NettyServer.java , WebSocketHandler.java
- 功能 :
- WebSocket 服务 :监听 WebSocket 连接(路径 /ws_pcm ),维持与 Web 前端的长连接。
- 信令交互 :解析连接 URL 中的 deviceId ,并在建立连接时调用外部接口( wvp-gb28181 )通知设备准备接收语音流。
- 音频接收 :接收前端发送的 Base64 编码的 PCM 音频数据。
1.1. 音频编解码层 (Audio Codec Layer)
- 核心类 : G711Codec.java , AudioCodec.java
- 功能 :
- 格式转换 :实现了 PCM (16-bit 8000Hz) 与 G.711 A-law (8-bit 8000Hz) 之间的双向转换算法。这是安防领域最通用的音频编码标准。
- 海思头处理 :具备识别和剔除海思私有音频头(Hisilicon Header)的能力,增强了对特定硬件的兼容性。
1.3. 媒体流传输层 (Media Transport Layer)
- 核心类 : BroadcastServer.java , RtpPack.java
- 功能 :
- RTP 封装 :将 G.711A 音频数据封装为 RTP (Real-time Transport Protocol) 包。 优化后的逻辑 现在能正确处理 RTP 头、序列号(Sequence Number)和时间戳(Timestamp),符合标准 RFC 3550。
- TCP 流式发送 :支持 RTP over TCP 模式(RFC 4571),每个广播会话启动一个独立的 TCP Server 进行推流。
- 动态端口 :为每个会话动态分配监听端口,支持多路并发广播。
1.4. 数据缓冲机制 (Buffering Strategy)
- 实现方式 :文件系统缓冲
- 流程 : WebSocketHandler 将转码后的音频写入磁盘文件 -> BroadcastServer 轮询读取文件发送。
- 特点 :利用磁盘文件作为简易的“消息队列”,解耦了 Web 接收线程和 TCP 发送线程。
2. 能力与数据流向
2.1. 核心能力
- Web 实时对讲 :用户无需安装插件,通过浏览器即可向监控设备喊话。
- 标准协议对接 :输出标准的 RTP 流,可对接海康、大华等主流安防设备及 GB28181 国标平台。
- 高并发基础 :基于 Netty NIO 框架,具备处理高并发连接的潜力(注:目前的磁盘 IO 缓冲方式可能是瓶颈,建议未来优化为内存队列)
2. 数据处理全流程
采集 PCM (Blob)
BinaryWebSocketFrame
转码 (PCM -> G.711A)
轮询读取
RTP 封装 (RtpPack)
Web 用户
Netty Server
WebSocketHandler
磁盘文件缓冲
BroadcastServer
TCP Stream
摄像机/国标平台
3. 前端实现 (web/):采集与二进制发送
前端核心逻辑位于 teach.realtime.encode_transfer_frame_pcm.js 中,主要负责音频的采集、切片和 直接二进制传输 。
- 音频采集与切片 :
- 使用 Recorder.js 采集 8000Hz 16bit PCM 音频。
- RealTimeSendTry 函数将音频流切分为固定大小的帧( SendFrameSize ,例如 3200 字节),确保发送频率稳定(约 100ms/帧)。
- 二进制发送 (优化点) :
- 传输层 : TransferUpload 函数中移除了 FileReader 转 Base64 的逻辑。
- WebSocket :直接调用 ws.send(blob) 发送 Blob 对象。浏览器底层会自动将其作为二进制帧(Binary Frame)处理, 带宽节省约 33% 。
- 配置 :显式设置 ws.binaryType = ‘arraybuffer’ 以便正确接收服务端回显(如有)。
以下是前端核心实现(音频采集、切片与二进制发送)的关键代码片段,位于 teach.realtime.encode_transfer_frame_pcm.js 中:
音频参数配置与切片
这段代码定义了采样率(与后端对齐为 8000Hz)、位深(16bit)和发送帧大小(3200字节,约 200ms),是保证音频流畅播放的基础。
var testSampleRate =8000;// 采样率必须与后端 G711Codec 一致var testBitRate =16;// 位深 16位// 每次发送指定二进制数据长度的数据帧,单位字节。// 16位 8000hz 的 pcm 1秒有:8000hz * 16位 / 8比特 = 16000 字节的数据// 配置 3200 字节则每秒发送大约 5 次 (200ms/帧)var SendFrameSize =3200;实时切片与转码 (RealTimeSendTry)
RealTimeSendTry 函数负责将采集到的 PCM 数据流缓存起来,凑够一帧( SendFrameSize )后才进行处理。虽然源数据本身就是 PCM,但为了统一流程,这里使用了 Recorder.mock 将数据封装为 Blob 对象。
// ... 从缓冲中切出一帧数据 ...// 满一帧了,清除已消费掉的缓冲if(pcmLen==chunkSize){ pcmOK=true;// ...}// ...// 16位pcm格式可以不经过mock转码,直接发送new Blob([pcm.buffer],{type:"audio/pcm"}) // 但为了通用性,这里使用 Recorder mock 接口统一封装为 Blobvar recMock=Recorder({type:"pcm",sampleRate:testSampleRate,// 8000bitRate:testBitRate // 16}); recMock.mock(pcm,pcmSampleRate); recMock.stop(function(blob,duration){// 转码/封装好就推入传输函数TransferUpload(number,blob,duration,recMock,false);// 递归调用,继续处理剩余数据RealTimeSendTry([],0, isClose);});二进制发送逻辑 (TransferUpload)
var ws;//=====数据传输函数==========varTransferUpload=function(number, blobOrNull, duration, blobRec, isClose){if(blobOrNull){var blob = blobOrNull;//*********发送方式二:Blob二进制发送 (推荐) ***************if(!ws){// 连接 WebSocket,带上设备ID ws =newWebSocket('ws://127.0.0.1:7211/ws_pcm?deviceId=填入设备id'); ws.binaryType ='arraybuffer';// 显式设置接收类型为二进制 ws.onopen=evt=>{ console.log("ws talk open (Binary Mode)");};// ... 错误处理与关闭逻辑 ...}if(ws && ws.readyState === WebSocket.OPEN){// 直接发送 Blob 对象,浏览器会自动处理为二进制帧 (Binary Frame)// 相比 Base64 节省约 33% 带宽 ws.send(blob); console.log("Sent binary blob, size: "+ blob.size);}}// ...};1. 音频参数配置与切片
这段代码定义了采样率(与后端对齐为 8000Hz)、位深(16bit)和发送帧大小(3200字节,约 200ms),是保证音频流畅播放的基础。
4. 后端接入 (src/):二进制帧处理与转码
后端核心逻辑位于 WebSocketHandler.java ,负责接收二进制流并进行转码存储。
- Netty 处理器升级 :
- 类定义改为 SimpleChannelInboundHandler ,同时支持 FullHttpRequest (握手)和 WebSocketFrame (数据)。
- handleWebSocketFrame 方法增加了对 BinaryWebSocketFrame 的支持。
- 零拷贝读取 :直接从 Netty 的 ByteBuf 中读取 PCM 字节流,避免了 Base64 解码的 CPU 消耗和内存分配。
- 音频转码 :
- 调用优化后的 G711Codec.java 的 encodeToG711A 方法。
- 将 16bit PCM (128kbps) 压缩为 8bit G.711A (64kbps),符合安防标准。
- 文件缓冲 :
- 将转码后的 G.711 数据写入本地磁盘({项目根目录}/recorders/{日期}/{端口}/),作为发送缓冲。
二进制帧处理 (Netty Inbound)
兼容 Text 帧(Base64)和 Binary 帧(Raw PCM),并实现 零拷贝 读取二进制数据。
// 支持处理多种类型的 WebSocket 帧publicclassWebSocketHandlerextendsSimpleChannelInboundHandler<Object>{privatevoidhandleWebSocketFrame(ChannelHandlerContext ctx,WebSocketFrame frame)throwsIOException{byte[] pcmData =null;if(frame instanceofTextWebSocketFrame){// 兼容:Base64 文本模式String text =((TextWebSocketFrame) frame).text(); pcmData =Base64Decoder.decode(text);}elseif(frame instanceofBinaryWebSocketFrame){ :二进制模式 (性能更优)// 直接从 Netty ByteBuf 读取数据,避免 Base64 解码开销ByteBuf content = frame.content(); pcmData =newbyte[content.readableBytes()]; content.readBytes(pcmData);}if(pcmData !=null&& pcmData.length >0){processPcmData(ctx, pcmData);}}// ...}音频转码与缓冲 (Transcode & Buffer)
收到 PCM 数据后,立即进行 G.711A 转码,并将结果写入磁盘文件作为缓冲。这是连接 Web 前端与 RTP 发送端的关键桥梁。
privatevoidprocessPcmData(ChannelHandlerContext ctx,byte[] pcmData)throwsIOException{// 转码 PCM -> G.711Abyte[] g711a =G711Codec.encodeToG711A(pcmData);String dirStr =getRecorderDir(server.getPort());FileUtil.mkdir(dirStr);// 使用时间戳作为文件名,注意:高并发下 System.currentTimeMillis() 可能重复,建议加随机数或原子计数器// 但这里是单连接单线程处理(Netty EventLoop),只要处理速度够快通常没问题,或者用 System.nanoTime()String fileName =System.currentTimeMillis()+".pcm";try(FileOutputStream out =newFileOutputStream(dirStr + fileName,false)){ out.write(g711a);}// 回显逻辑 (可选)/* String deviceId = ctx.channel().attr(key).get(); if (deviceId != null) { List<Channel> channelList = getChannelByName(deviceId); // 注意:回显通常不需要发回给自己,或者是为了调试 // 如果要发回二进制,需要用 BinaryWebSocketFrame // channelList.forEach(channel -> channel.writeAndFlush(new BinaryWebSocketFrame(Unpooled.copiedBuffer(pcmData)))); } */}连接握手与信令触发 (Handshake & Signaling)
在建立 WebSocket 连接时,解析 URL 参数并触发外部 SIP 信令(通知摄像机/平台准备接收)。
privatevoidhandleHttpRequest(ChannelHandlerContext ctx,FullHttpRequest request){// 启动对应的 RTP 推流服务 server =newBroadcastServer(); server.start();// 解析 deviceIdMap<String,String> paramMap =getUrlParams(request.uri());String deviceId = paramMap.get("deviceId");if(deviceId !=null){online(deviceId, ctx.channel());// 调用外部 WVP 接口,通过 SIP 信令通知设备连接 BroadcastServer 的端口try{HttpRequest.get("https://127.0.0.1:8843/api/play/broadcast/"+ deviceId).execute(true);}catch(Exception e){System.err.println("调用 WVP 接口失败: "+ e.getMessage());}}}4. 推流服务 (src/):RTP 封装与 TCP 推送
这部分由 BroadcastServer.java 和 RtpPack.java 负责,确保输出符合 GB28181 标准的流。
- RTP 封装 (已修复) :
- RtpPack 类负责将 G.711 音频流封装为 RTP 包。
- 头部修正 :现在能正确生成 12字节 RTP 头 + 2字节 TCP 长度头 。
- 时间戳同步 :根据数据长度动态计算 RTP 时间戳,确保播放连续、不卡顿、不变调。
- TCP 推流 :
- BroadcastServer 启动一个独立的 TCP Server(动态端口)。
- 智能轮询 (优化点) :循环读取缓冲目录下的新文件。如果暂无新文件,线程会短暂休眠( Thread.sleep ),防止 CPU 空转。
- 读取到的数据经过 RTP 封装后,直接通过 Socket 发送给连接的摄像机或平台。
5. 总结
该工程是一个 轻量级的 Web 对讲/广播网关系统。它利用 Netty 的高性能网络处理能力,实现了从 Web 前端采集音频,经过服务端转码与协议封装,最终以标准 GB28181/RTP 协议推送到前端设备(如摄像机、NVR 或国标平台)的核心功能。
- 前端 : PCM -> Blob -> WebSocket (Binary)
- 后端 : BinaryFrame -> PCM -> G.711A -> File -> RTP -> TCP