欢迎光临
我们一直在努力

Web对讲/广播功能

Web对讲/广播功能

  • 介绍
      • 1. 核心功能模块
        • 1.1. Web 接入与信令层 (Web Access Layer)
        • 1.1. 音频编解码层 (Audio Codec Layer)
        • 1.3. 媒体流传输层 (Media Transport Layer)
        • 1.4. 数据缓冲机制 (Buffering Strategy)
      • 2. 能力与数据流向
        • 2.1. 核心能力
        • 2. 数据处理全流程
      • 3. 前端实现 (web/):采集与二进制发送
        • 音频参数配置与切片
        • 实时切片与转码 (RealTimeSendTry)
        • 二进制发送逻辑 (TransferUpload)
      • 1. 音频参数配置与切片
      • 4. 后端接入 (src/):二进制帧处理与转码
        • 二进制帧处理 (Netty Inbound)
        • 音频转码与缓冲 (Transcode & Buffer)
        • 连接握手与信令触发 (Handshake & Signaling)
      • 4. 推流服务 (src/):RTP 封装与 TCP 推送
      • 5. 总结

介绍

轻量级的 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. 数据处理全流程

    #mermaid-svg-cG3zYllf3znopfvM{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cG3zYllf3znopfvM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cG3zYllf3znopfvM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cG3zYllf3znopfvM .error-icon{fill:#552222;}#mermaid-svg-cG3zYllf3znopfvM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cG3zYllf3znopfvM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cG3zYllf3znopfvM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cG3zYllf3znopfvM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cG3zYllf3znopfvM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cG3zYllf3znopfvM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cG3zYllf3znopfvM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cG3zYllf3znopfvM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cG3zYllf3znopfvM .marker.cross{stroke:#333333;}#mermaid-svg-cG3zYllf3znopfvM svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cG3zYllf3znopfvM p{margin:0;}#mermaid-svg-cG3zYllf3znopfvM .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-cG3zYllf3znopfvM .cluster-label text{fill:#333;}#mermaid-svg-cG3zYllf3znopfvM .cluster-label span{color:#333;}#mermaid-svg-cG3zYllf3znopfvM .cluster-label span p{background-color:transparent;}#mermaid-svg-cG3zYllf3znopfvM .label text,#mermaid-svg-cG3zYllf3znopfvM span{fill:#333;color:#333;}#mermaid-svg-cG3zYllf3znopfvM .node rect,#mermaid-svg-cG3zYllf3znopfvM .node circle,#mermaid-svg-cG3zYllf3znopfvM .node ellipse,#mermaid-svg-cG3zYllf3znopfvM .node polygon,#mermaid-svg-cG3zYllf3znopfvM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cG3zYllf3znopfvM .rough-node .label text,#mermaid-svg-cG3zYllf3znopfvM .node .label text,#mermaid-svg-cG3zYllf3znopfvM .image-shape .label,#mermaid-svg-cG3zYllf3znopfvM .icon-shape .label{text-anchor:middle;}#mermaid-svg-cG3zYllf3znopfvM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cG3zYllf3znopfvM .rough-node .label,#mermaid-svg-cG3zYllf3znopfvM .node .label,#mermaid-svg-cG3zYllf3znopfvM .image-shape .label,#mermaid-svg-cG3zYllf3znopfvM .icon-shape .label{text-align:center;}#mermaid-svg-cG3zYllf3znopfvM .node.clickable{cursor:pointer;}#mermaid-svg-cG3zYllf3znopfvM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cG3zYllf3znopfvM .arrowheadPath{fill:#333333;}#mermaid-svg-cG3zYllf3znopfvM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cG3zYllf3znopfvM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cG3zYllf3znopfvM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cG3zYllf3znopfvM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cG3zYllf3znopfvM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cG3zYllf3znopfvM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cG3zYllf3znopfvM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cG3zYllf3znopfvM .cluster text{fill:#333;}#mermaid-svg-cG3zYllf3znopfvM .cluster span{color:#333;}#mermaid-svg-cG3zYllf3znopfvM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cG3zYllf3znopfvM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cG3zYllf3znopfvM rect.text{fill:none;stroke-width:0;}#mermaid-svg-cG3zYllf3znopfvM .icon-shape,#mermaid-svg-cG3zYllf3znopfvM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cG3zYllf3znopfvM .icon-shape p,#mermaid-svg-cG3zYllf3znopfvM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cG3zYllf3znopfvM .icon-shape rect,#mermaid-svg-cG3zYllf3znopfvM .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cG3zYllf3znopfvM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cG3zYllf3znopfvM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cG3zYllf3znopfvM :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    采集 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 接口统一封装为 Blob
    var recMock=Recorder({
    type:"pcm",
    sampleRate:testSampleRate, // 8000
    bitRate:testBitRate // 16
    });
    recMock.mock(pcm,pcmSampleRate);
    recMock.stop(function(blob,duration){
    // 转码/封装好就推入传输函数
    TransferUpload(number,blob,duration,recMock,false);

    // 递归调用,继续处理剩余数据
    RealTimeSendTry([], 0, isClose);
    });

    二进制发送逻辑 (TransferUpload)

    var ws;
    //=====数据传输函数==========
    var TransferUpload = function(number, blobOrNull, duration, blobRec, isClose){
    if(blobOrNull){
    var blob = blobOrNull;

    //*********发送方式二:Blob二进制发送 (推荐) ***************
    if(!ws) {
    // 连接 WebSocket,带上设备ID
    ws = new WebSocket('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 帧
    public class WebSocketHandler extends SimpleChannelInboundHandler<Object> {

    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) throws IOException {
    byte[] pcmData = null;

    if (frame instanceof TextWebSocketFrame) {
    // 兼容:Base64 文本模式
    String text = ((TextWebSocketFrame) frame).text();
    pcmData = Base64Decoder.decode(text);
    } else if (frame instanceof BinaryWebSocketFrame) {
    :二进制模式 (性能更优)
    // 直接从 Netty ByteBuf 读取数据,避免 Base64 解码开销
    ByteBuf content = frame.content();
    pcmData = new byte[content.readableBytes()];
    content.readBytes(pcmData);
    }

    if (pcmData != null && pcmData.length > 0) {
    processPcmData(ctx, pcmData);
    }
    }
    // …
    }

    音频转码与缓冲 (Transcode & Buffer)

    收到 PCM 数据后,立即进行 G.711A 转码,并将结果写入磁盘文件作为缓冲。这是连接 Web 前端与 RTP 发送端的关键桥梁。

    private void processPcmData(ChannelHandlerContext ctx, byte[] pcmData) throws IOException {
    // 转码 PCM -> G.711A
    byte[] 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 = new FileOutputStream(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 信令(通知摄像机/平台准备接收)。

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
    // 启动对应的 RTP 推流服务
    server = new BroadcastServer();
    server.start();

    // 解析 deviceId
    Map<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

    代码示例 – github地址

    赞(0)
    未经允许不得转载:171主机测评 » Web对讲/广播功能
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址