基于websocket实现本地web语音聊天

基于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;
}
相关推荐
Shota Kishi11 分钟前
Solana Shreds 的 UDP 直投架构:Shredstream UDP Forwarding 的设计与实现
网络协议·架构·udp·区块链·solana
AlienZHOU21 分钟前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
李昊哲小课2 小时前
Spring Boot SSE 实时推送教程
spring boot·websocket·flux·sseemitter
Captaincc4 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师5 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen5 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒6 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow6 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8186 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800786 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端