基于libwebsockets库实现语音聊天
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(){
// 读取的ArrayBuffer
const arrayBuffer = this.result;
// 解码ArrayBuffer数据
audioContext.decodeAudioData(arrayBuffer, function(audioData){
// 创建音频缓冲区源节点
const source = audioContext.createBufferSource();
source.buffer = audioData;
source.connect(audioContext.destination);
source.start(); // 播放音频
});
};
}
// 读取Blob为ArrayBuffer
reader.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 100
struct 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
", 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', ''};
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;
}