由于浏览器不能直接连接udp服务,所以需要搭建一个websocket服务做中转,让websocket服务连接udp服务
1、vue开发获取实时音频数据并按4096分包后添加rtp协议头发送到websocket服务(连接websocket自行编写连接到127.0.0.1:8889)
data(){
return {
audioContext:null,
rc:null,
}
},
methods:{
startRecorder(){
let that = this;
//可以用下面的代码来边讲话边听
const audio = new Audio()
audio.autoplay = true
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
that.rc = stream
audio.srcObject = stream
that.audioContext = new AudioContext();
const mediaStreamSource = that.audioContext.createMediaStreamSource(stream);
const bufferSize = 4096; // 根据需要选择缓冲区大小
const scriptNode = that.audioContext.createScriptProcessor(bufferSize, 1, 1);
// 当音频处理事件发生时
scriptNode.onaudioprocess = (event) => {
const inputBuffer = event.inputBuffer;
const inputData = inputBuffer.getChannelData(0);
// 将音频数据编码为 RTP 协议头的 16 位二进制数据
const rtpData = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
rtpData[i] = inputData[i] * 32767; // 缩放到 16 位范围
}
//原数据的二进制数据
const rtpBinary = rtpData.buffer;
//发送原数据到websocket服务
// websocketsend(rtpBinary);
//下面是创建rtp协议头的,可选
// 创建 RTP 协议头
const version = 2; // RTP 版本
const padding = 0; // 填充位
const extension = 0; // 扩展位
const csrcCount = 0; // CSRC 计数
const marker = 0; // 标记位
const payloadType = 8; // 负载类型 8:PCMA
let sequenceNumber = 0;//序列号(根据需要修改)
let timestamp = 0;//开始截取时间戳(根据需要修改)
const sampleRate = 44100; // 音频采样率(根据需要修改)
const ssrc = Math.floor(Math.random() * 0xFFFFFFFF);//同步源标识符, 32位的无符号整数(根据需要修改)
// 根据需要的时间位置和采样率计算时间戳
timestamp += (bufferSize / sampleRate) * 90000; // bufferSize 是 RTP 包的大小
const rtpHeader = new Uint8Array(12); // RTP 协议头为12字节
const view = new DataView(rtpHeader.buffer);
view.setUint8(0, (version << 6) | (padding << 5) | (extension << 4) | csrcCount);//<<左移操作符
view.setUint8(1, (marker << 7) | payloadType & 0x7F);
//view.setUint8(1, (marker << 7) | payloadType);
view.setUint16(2, sequenceNumber);
view.setUint32(4, timestamp);
view.setUint32(8, ssrc);
// 将 RTP 协议头和 rtpBinary 合并为一个数据包
const rtpPacket = new Uint8Array(rtpHeader.length + rtpBinary.byteLength);
rtpPacket.set(rtpHeader);
rtpPacket.set(new Uint8Array(rtpBinary), rtpHeader.length);
sequenceNumber = (sequenceNumber + 1) & 0xFFFF;
// 发送 RTP 数据到 WebSocket 服务器
websocketsend(rtpPacket);
};
mediaStreamSource.connect(scriptNode);
scriptNode.connect(that.audioContext.destination);
})
.catch(error => alert(error));
},
//停止采集音频
stopRecorder(){
const audioTrack = this.rc?.getAudioTracks()[0];
if(audioTrack){
audioTrack.stop();
}
if(this.rc){
this.rc = null
this.audioContext.close()
this.audioContext = null
//停止websocket连接
websocketclose()
}
}
}
2、使用nodejs搭建websocket服务
创建一个app.js,并安装dgram依赖 npm install dgram --save
const WebSocket = require('ws');
const dgram = require('dgram');
// 创建WebSocket服务器
const wss = new WebSocket.Server({ port: 8889 });
// 创建UDP套接字
const udpSocket = dgram.createSocket('udp4');
// 监听WebSocket连接
wss.on('connection', (ws) => {
console.log('客户端已连接');
// 监听WebSocket消息
ws.on('message', (message) => {
console.log('收到消息:', message);
// 发送消息到UDP服务器
udpSocket.send(message, 0, message.length, 8888, '127.0.0.1', (err) => {
if (err) {
console.error('发送UDP消息失败:', err);
}
});
});
// 监听UDP消息
udpSocket.on('message', (message, rinfo) => {
console.log('收到UDP消息:', message.toString());
// 将UDP消息发送给WebSocket客户端
ws.send(message.toString());
});
// 监听WebSocket关闭
ws.on('close', () => {
console.log('客户端已断开连接');
});
});
// 开始监听UDP端口
udpSocket.bind(8888, '127.0.0.1', () => {
console.log('UDP套接字已绑定');
});
运行app.js: node app.js
3、udp服务上可以看到发送过来的二进制数据(调试工具在这百度网盘地址)
注:如果提示[Deprecation] The ScriptProcessorNode is deprecated. Use AudioWorkletNode instead. (https://bit.ly/audio-worklet)是因为ScriptProcessorNode已经弃用了,可以不用更改,多了个警告而已,不影响。如需要更换为AudioWorkletNode,以下就是更换后的代码
1、新建一个audioworklet.js
class MyAudioWorkletProcessor extends AudioWorkletProcessor {
constructor() {
super();
// 在这里初始化任何需要的变量,如序列号、时间戳、SSRC 等
this.sequenceNumber = 0;
this.timestamp = 0;
this.ssrc = Math.floor(Math.random() * 0xFFFFFFFF);
}
process(inputs, outputs, parameters) {
const input = inputs[0];
const inputData = input[0]; // 假设只有一个输入通道
// 创建 RTP 协议头
const rtpHeader = new Uint8Array(12); // RTP 协议头为12字节
const view = new DataView(rtpHeader.buffer);
const version = 2; // RTP 版本
const padding = 0; // 填充位
const extension = 0; // 扩展位
const csrcCount = 0; // CSRC 计数
const marker = 0; // 标记位
const payloadType = 8; // 负载类型,根据需要更改
view.setUint8(0, (version << 6) | (padding << 5) | (extension << 4) | csrcCount);
view.setUint8(1, (marker << 7) | payloadType);
view.setUint16(2, this.sequenceNumber);
view.setUint32(4, this.timestamp);
view.setUint32(8, this.ssrc);
// 将 RTP 协议头和音频数据合并为一个数据包
const rtpPacket = new Uint8Array(rtpHeader.length + inputData.length);
rtpPacket.set(rtpHeader);
rtpPacket.set(new Uint8Array(inputData.buffer), rtpHeader.length);
console.log(rtpPacket);
// 发送 RTP 数据到 WebSocket 服务器
// 你需要将这个数据包发送到 WebSocket 服务器,可能需要一个 WebSocket 连接来发送数据
// WebSocket 发送代码应该放在这里
// 更新序列号和时间戳
this.sequenceNumber = (this.sequenceNumber + 1) & 0xFFFF;
this.timestamp += inputData.length;
return true;
}
}
registerProcessor('my-audio-worklet-processor', MyAudioWorkletProcessor);
然后把上面的采集函数改一下,提示一下,这个可能需要在https的网站使用
startRecorder(){
let that = this;
//可以用下面的代码来边讲话边听
const audio = new Audio()
audio.autoplay = true
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
that.rc = stream
audio.srcObject = stream
that.audioContext = new AudioContext();
const mediaStreamSource = that.audioContext.createMediaStreamSource(stream);
that.audioContext.audioWorklet.addModule('audioworklet.js')
.then(() => {
// 创建 AudioWorkletNode
const workletNode = new AudioWorkletNode(that.audioContext, 'my-audio-worklet-processor');
// 连接音频输入和输出
mediaStreamSource.connect(workletNode);
workletNode.connect(that.audioContext.destination);
// 音频处理逻辑已在 audioworklet.js 中定义
})
.catch((error) => {
console.error('加载音频工作线程失败:', error);
});
mediaStreamSource.connect(scriptNode);
scriptNode.connect(that.audioContext.destination);
})
.catch(error => alert(error));
}