基于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;
}
相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60617 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角8 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!8 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
脉动数据行情19 小时前
C# 实现德指 DAX 期货 WebSocket 实时行情监听
开发语言·websocket·c#
mayaairi10 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端