基于libwebsockets库实现语音聊天
1、关于libwebsocket库自行编译2、client使用html+js 代码3、服务端代码
1、关于libwebsocket库自行编译
2、client使用html+js 代码
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>多人语音聊天</title></head><body> <h1>多人语音聊天</h1> 服务器地址: <input type="text" id="serverAddress" placeholder="ws://10.114.139.161:12345"> <button id="connectButton">连接</button> <button id="startButton" disabled>开始</button> <button id="stopButton" disabled>停止</button> <audio id="audio" autoplay></audio> <script> const audio = document.getElementById('audio'); const serverAddress = document.getElementById('serverAddress'); const connectButton = document.getElementById('connectButton'); const startButton = document.getElementById('startButton'); const stopButton = document.getElementById('stopButton'); let ws = null; let mediaRecorder; let audioContext; let source;let reader;let receivedBlobs = []; // 用于缓存ArrayBuffer片段 connectButton.addEventListener('click', () => { const url = serverAddress.value; if (url) { ws = new WebSocket(url); ws.onopen = () => { console.log('连接到服务器'); connectButton.disabled = true; startButton.disabled = false; }; ws.onmessage = (event) => {if (event.data instanceof Blob) {receivedBlobs.push(event.data)console.log('Received message length:', event.data.size);} else {const combinedBlob = new Blob(receivedBlobs, { type: 'audio/opus' });playAudio(combinedBlob);receivedBlobs.fill(null);receivedBlobs.length = 0;// 如果接收到的是其他类型的数据,可以在这里处理console.log('Received message:', event.data);} }; ws.onclose = () => { console.log('与服务器断开连接'); connectButton.disabled = false; startButton.disabled = true; stopButton.disabled = true; if (mediaRecorder) { mediaRecorder.stop(); } }; ws.onerror = (error) => { console.error('WebSocket错误:', error); }; } else { console.error('请输入服务器地址'); } }); startButton.addEventListener('click', () => { if (!ws || ws.readyState !== WebSocket.OPEN) { console.error('WebSocket连接未打开'); return; } startRecording(); startButton.disabled = true; stopButton.disabled = false; }); stopButton.addEventListener('click', () => { if (mediaRecorder) { mediaRecorder.stop(); } startButton.disabled = false; stopButton.disabled = true; }); function startRecording() { navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { mediaRecorder = new MediaRecorder(stream); mediaRecorder.ondataavailable = event => { if (event.data.size > 0) { ws.send(event.data); } }; mediaRecorder.start(); }) .catch(error => { console.error('无法获取麦克风权限', error); }); } function playAudio(data) { if (!audioContext) { audioContext = new (window.AudioContext || webkitAudioContext)();reader = new FileReader();reader.onload = function(){ // 读取的ArrayBufferconst arrayBuffer = this.result;// 解码ArrayBuffer数据audioContext.decodeAudioData(arrayBuffer, function(audioData){// 创建音频缓冲区源节点const source = audioContext.createBufferSource();source.buffer = audioData;source.connect(audioContext.destination);source.start(); // 播放音频});}; }// 读取Blob为ArrayBufferreader.readAsArrayBuffer(data); } function mergeArrayBuffers(...arrays) { let totalLength = arrays.reduce((acc, arrayBuffer) => acc + arrayBuffer.byteLength, 0); let result = new Uint8Array(totalLength); let offset = 0; for (let arrayBuffer of arrays) { result.set(new Uint8Array(arrayBuffer), offset); offset += arrayBuffer.byteLength; } return result.buffer; } </script></body></html>
3、服务端代码
#include "libwebsockets.h"#include <signal.h>#include <iostream>#include <netinet/in.h>#include <sys/socket.h>#include <arpa/inet.h>#include <fstream>static volatile int exit_sig = 0;#define LOCALHOST_LEN 256#define MAX_PAYLOAD_SIZE 10 * 1024#define MAX_CLIENTS 100struct lws *active_clients[MAX_CLIENTS] = {0};int num_active_clients = 0;void sighdl(int sig){ lwsl_notice("%d traped", sig); exit_sig = 1;}/** * 会话上下文对象,结构根据需要自定义 */struct session_data{ int msg_count; unsigned char buf[LWS_PRE + MAX_PAYLOAD_SIZE]; int len; bool bin; bool fin;};static int protocol_my_callback(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len){ struct session_data *data = (struct session_data *)user; switch (reason) { case LWS_CALLBACK_ESTABLISHED: // 当服务器和客户端完成握手后 { char ip[LOCALHOST_LEN]; lws_get_peer_simple(wsi, ip, sizeof(ip)); printf("ip: %s \n", ip); if (num_active_clients < MAX_CLIENTS) { active_clients[num_active_clients++] = wsi; } std::cout << "ws client connect cur client num: " << num_active_clients << std::endl; break; } case LWS_CALLBACK_RECEIVE: // 当接收到客户端发来的帧以后 { // 判断是否最后一帧 bool is_last_frame = lws_is_final_fragment(wsi); bool is_bin = lws_frame_is_binary(wsi); std::cout << "receive msg size: " << len << " is last: " << is_last_frame << " is_bin: " << is_bin << std::endl; for (int i = 0; i < num_active_clients; i++) { // 跳过发送者自身 if (active_clients[i] != wsi) { std::cout << "send client msg size: " << len << std::endl; lws_write(active_clients[i], (unsigned char *)in, len, LWS_WRITE_BINARY); if (is_last_frame) { unsigned char end[] = {'e', 'n', 'd', '\0'}; lws_write(active_clients[i], end, sizeof(end), LWS_WRITE_TEXT); } } } break; } case LWS_CALLBACK_CLOSED: // 当此连接可写时 { for (int i = 0; i < num_active_clients; i++) { if (active_clients[i] == wsi) { active_clients[i] = active_clients[num_active_clients - 1]; num_active_clients--; break; } } std::cout << "ws client close cur client num: " << num_active_clients << std::endl; break; } } // 回调函数最终要返回0,否则无法创建服务器 return 0;}/** * 支持的WebSocket子协议数组 * 子协议即JavaScript客户端WebSocket(url, protocols)第2参数数组的元素 * 你需要为每种协议提供回调函数 */struct lws_protocols protocols[] = { { // 协议名称,协议回调,接收缓冲区大小 "ws", protocol_my_callback, sizeof(struct session_data), MAX_PAYLOAD_SIZE, }, { NULL, NULL, 0 // 最后一个元素固定为此格式 }};int main(int agrc, char *argv[]){ // 信号处理函数 signal(SIGTERM, sighdl); struct lws_context_creation_info ctx_info = {0}; ctx_info.port = 12345; ctx_info.iface = nullptr; ctx_info.protocols = protocols; ctx_info.gid = -1; ctx_info.uid = -1; ctx_info.options = LWS_SERVER_OPTION_VALIDATE_UTF8; struct lws_context *context = lws_create_context(&ctx_info); while (!exit_sig) { lws_service(context, 1000); } lws_context_destroy(context); return 0;}