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

基于ICE-SERVER的P2P连接:无服务器实现双HTML页面音频传输

无服务器实现跨页面音频传输与播放

要实现同浏览器下两个标签页之间的音频收发,无需额外服务器,直接用浏览器原生的Broadcast Channel API就能搞定——它专门用于同源页面(本地file://协议的HTML文件属于同源)之间的通信。下面是完整的实现代码:

1. 音频发送页面(sender.html)

这个页面支持选择本地音频文件或录制音频,然后通过广播通道发送给接收页面。

<!DOCTYPE html>
<html>
<head>
    <title>音频发送端</title>
</head>
<body>
    <h3>选择或录制音频发送</h3>
    <input type="file" accept="audio/*" id="audioFile">
    <button id="startRecord">开始录制</button>
    <button id="stopRecord">停止录制并发送</button>

    <script>
        // 创建广播通道,通道名需与接收端一致
        const broadcastChannel = new BroadcastChannel('audio-channel');
        let mediaRecorder;
        let audioChunks = [];

        // 选择本地音频文件发送
        document.getElementById('audioFile').addEventListener('change', async (e) => {
            const file = e.target.files[0];
            if (!file) return;
            
            // 将文件转为ArrayBuffer发送
            const arrayBuffer = await file.arrayBuffer();
            broadcastChannel.postMessage({
                type: 'audio',
                data: arrayBuffer
            });
            alert('音频已发送');
        });

        // 启动音频录制
        document.getElementById('startRecord').addEventListener('click', async () => {
            const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
            mediaRecorder = new MediaRecorder(stream);
            
            mediaRecorder.ondataavailable = (e) => {
                audioChunks.push(e.data);
            };
            
            mediaRecorder.start();
            alert('开始录制');
        });

        // 停止录制并发送音频
        document.getElementById('stopRecord').addEventListener('click', () => {
            if (!mediaRecorder) return;
            
            mediaRecorder.stop();
            mediaRecorder.onstop = () => {
                const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
                audioBlob.arrayBuffer().then(arrayBuffer => {
                    broadcastChannel.postMessage({
                        type: 'audio',
                        data: arrayBuffer
                    });
                    alert('录制的音频已发送');
                    audioChunks = [];
                });
            };
        });
    </script>
</body>
</html>

2. 音频接收页面(receiver.html)

这个页面监听广播通道,收到音频数据后解码并自动播放。

<!DOCTYPE html>
<html>
<head>
    <title>音频接收端</title>
</head>
<body>
    <h3>等待接收音频...</h3>
    <audio id="audioPlayer" controls></audio>

    <script>
        // 创建与发送端同名的广播通道
        const broadcastChannel = new BroadcastChannel('audio-channel');
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const audioPlayer = document.getElementById('audioPlayer');

        broadcastChannel.onmessage = async (e) => {
            if (e.data.type !== 'audio') return;
            
            // 解码音频数据
            const audioBuffer = await audioContext.decodeAudioData(e.data.data);
            
            // 将AudioBuffer转为WAV格式Blob,适配audio元素播放
            const wavBlob = audioBufferToWav(audioBuffer);
            const audioUrl = URL.createObjectURL(wavBlob);
            
            audioPlayer.src = audioUrl;
            audioPlayer.play().catch(err => console.error('播放失败:', err));
        };

        // 辅助函数:将AudioBuffer转为WAV格式Blob
        function audioBufferToWav(buffer) {
            const length = buffer.length * buffer.numberOfChannels * 2 + 44;
            const arrayBuffer = new ArrayBuffer(length);
            const view = new DataView(arrayBuffer);
            const channels = [];
            let offset = 0;
            let pos = 0;

            // 写入WAV文件头
            const setUint16 = (data) => {
                view.setUint16(pos, data, true);
                pos += 2;
            };
            const setUint32 = (data) => {
                view.setUint32(pos, data, true);
                pos += 4;
            };

            setUint32(0x46464952); // "RIFF"
            setUint32(length - 8); // 文件总长度-8
            setUint32(0x45564157); // "WAVE"

            setUint32(0x20746d66); // "fmt "
            setUint32(16); // PCM格式块大小
            setUint16(1); // PCM编码类型
            setUint16(buffer.numberOfChannels); // 声道数
            setUint32(buffer.sampleRate); // 采样率
            setUint32(buffer.sampleRate * 2 * buffer.numberOfChannels); // 字节率
            setUint16(buffer.numberOfChannels * 2); // 块对齐
            setUint16(16); // 位深度

            setUint32(0x61746164); // "data"
            setUint32(length - pos - 4); // 音频数据长度

            // 写入音频采样数据
            for (let i = 0; i < buffer.numberOfChannels; i++) {
                channels.push(buffer.getChannelData(i));
            }

            while (pos < length) {
                for (let i = 0; i < buffer.numberOfChannels; i++) {
                    let sample = Math.max(-1, Math.min(1, channels[i][offset]));
                    sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
                    view.setInt16(pos, sample, true);
                    pos += 2;
                }
                offset++;
            }

            return new Blob([arrayBuffer], { type: 'audio/wav' });
        }
    </script>
</body>
</html>

关键注意事项

  • 同源限制:两个页面必须在同一浏览器打开,且都是本地file://协议(或同一域名),否则Broadcast Channel无法通信。
  • 浏览器权限:
    • 录制功能需要麦克风权限,首次打开会弹窗请求授权。
    • 部分浏览器(如Chrome)本地打开文件时,需添加启动参数--allow-file-access-from-files,否则可能无法读取本地文件或调用麦克风。
  • 兼容性:Broadcast Channel API支持所有现代浏览器,无需额外依赖。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 08:22:21