You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

浏览器实时播放IP摄像头ADPCM音频流及停止功能实现咨询

需求与现有问题

我有一台推送非标准ADPCM音频流的IP摄像头,音频块固定为544字节,每个块包含32字节头部需移除。现有Firefox端JS代码能接收、处理音频,但存在两个问题:

  • 需缓存指定数量的音频块后才一次性播放,无法实时连续播放
  • 缺少停止播放的功能

核心改进方向

  • 采用实时音频处理节点:替代一次性缓存后播放的逻辑,用ScriptProcessorNode(兼容性优先)或AudioWorklet(现代浏览器推荐)实现流式音频输出
  • 添加全局控制机制:保存流读取器、音频上下文、处理节点的引用,通过标记位终止流读取并释放音频资源
  • 移除固定块数限制:改为无限循环读取流,直到触发停止操作

修改后的实现代码

let form = document.forms.audioStreamSettings;
let inputAudioStreamURL = form.elements.audioStreamURL;
let btnStartStream = form.elements.startStream;
let btnStopStream = form.elements.stopStream;

// 全局控制变量
let audioCtx = null;
let audioProcessor = null;
let streamReader = null;
let isStreaming = false;
let pcmDataQueue = [];

// 初始化实时音频处理节点
function initAudioPipeline() {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 8000 });
    // 创建ScriptProcessorNode,缓冲区大小需为2的幂(如1024)
    audioProcessor = audioCtx.createScriptProcessor(1024, 1, 1);

    audioProcessor.onaudioprocess = function(e) {
        const outputChannel = e.outputBuffer.getChannelData(0);
        let writeOffset = 0;

        // 从队列中取数据填充输出缓冲区
        while (writeOffset < outputChannel.length && pcmDataQueue.length > 0) {
            const currentChunk = pcmDataQueue[0];
            const copySize = Math.min(currentChunk.length, outputChannel.length - writeOffset);
            
            outputChannel.set(currentChunk.subarray(0, copySize), writeOffset);
            writeOffset += copySize;

            // 更新队列中的剩余数据
            if (currentChunk.length > copySize) {
                pcmDataQueue[0] = currentChunk.subarray(copySize);
            } else {
                pcmDataQueue.shift();
            }
        }

        // 剩余位置填充静音
        while (writeOffset < outputChannel.length) {
            outputChannel[writeOffset++] = 0;
        }
    };

    audioProcessor.connect(audioCtx.destination);
}

btnStartStream.addEventListener('click', async () => {
    if (isStreaming) return;
    
    const streamURL = inputAudioStreamURL.value;
    isStreaming = true;

    // 初始化音频管道,激活音频上下文(浏览器要求用户交互触发)
    if (!audioCtx) initAudioPipeline();
    if (audioCtx.state === 'suspended') await audioCtx.resume();

    try {
        const response = await fetch(streamURL);
        streamReader = response.body.getReader();

        while (isStreaming) {
            const { done, value } = await streamReader.read();
            if (done || !isStreaming) break;

            const rawChunk = new Uint8Array(value);
            // 移除32字节头部
            const adpcmChunk = rawChunk.slice(32, 544);
            // 转换ADPCM到Float32格式的PCM
            const pcmChunk = decodeAdpcm(adpcmChunk);
            
            // 将转换后的数据加入播放队列
            pcmDataQueue.push(pcmChunk);
        }
    } catch (err) {
        console.error('音频流读取异常:', err);
    } finally {
        stopStream();
    }
});

btnStopStream.addEventListener('click', stopStream);

// 停止流播放并释放资源
function stopStream() {
    isStreaming = false;
    if (streamReader) streamReader.cancel();
    if (audioProcessor) {
        audioProcessor.disconnect();
        audioProcessor = null;
    }
    if (audioCtx) {
        audioCtx.close();
        audioCtx = null;
    }
    pcmDataQueue = [];
    console.log('音频流已停止');
}

// 此处替换为实际的ADPCM解码逻辑
function decodeAdpcm(adpcmData) {
    // 根据摄像头的ADPCM编码规则实现转换,输出单声道、8000采样率的Float32Array
    // 示例框架,需根据实际编码调整
    const pcmLength = adpcmData.length * 2; // 假设1字节ADPCM对应2字节PCM
    const pcmBuffer = new Float32Array(pcmLength);
    // ... 解码逻辑 ...
    return pcmBuffer;
}

关键逻辑说明

  1. 流式播放:通过ScriptProcessorNode的onaudioprocess回调,实时从PCM队列中取数据填充音频输出缓冲区,实现边处理边播放
  2. 停止控制:用isStreaming标记位终止流读取循环,调用reader.cancel()停止接收流,同时断开音频节点并关闭上下文释放资源
  3. 队列缓冲:用数组维护待播放的PCM数据,避免因网络或解码延迟导致的播放卡顿
  4. 上下文激活:处理浏览器的音频自动暂停限制,通过用户交互唤醒音频上下文

内容的提问来源于stack exchange,提问作者alexeynl

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 15:04:56