commit 7703a266bf05a021f9f72916c9268caa468f31dc
Author: Song367 <601337784@qq.com>
Date: Wed Jul 23 15:55:48 2025 +0800
commit list
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e43b18c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,185 @@
+# WebRTC 音视频通话应用
+
+一个基于WebRTC的实时音视频通话web应用,使用WebRTC技术传输视频流。用户开始通话后会自动播放默认视频流,当用户进行音频或文本对话时会切换到相应的视频流,交互结束后自动回到默认视频流。
+
+## 功能特性
+
+- 🎤 **实时音频通话**: 基于WebRTC技术实现点对点音频通话
+- 📹 **WebRTC视频流传输**: 使用WebRTC技术传输视频流,而非播放本地文件
+- 📐 **固定视频比例**: 视频播放框固定9:16比例,适合移动端显示
+- 🎬 **无播放条界面**: 隐藏视频播放条,提供纯净的观看体验
+- 💬 **文本输入识别**: 根据输入的文本内容切换相应视频流
+- 🎤 **语音输入识别**: 支持语音输入并切换对应视频流
+- 🔄 **自动回退**: 交互结束后自动回到默认视频流
+- 🎨 **现代化UI**: 响应式设计,支持移动端和桌面端
+- 🔄 **实时同步**: 所有用户实时同步视频流状态
+
+## WebRTC视频流传输机制
+
+1. **视频流创建**: 将本地视频文件转换为MediaStream
+2. **Canvas渲染**: 使用Canvas将视频渲染为实时流
+3. **流切换**: 根据用户交互动态切换不同的视频流
+4. **实时同步**: 所有用户实时同步视频流状态
+5. **缓存机制**: 缓存已创建的视频流,提高切换效率
+6. **多用户传输**: 通过WebRTC技术向所有连接用户传输视频流
+
+## 技术栈
+
+- **前端**: HTML5, CSS3, JavaScript (ES6+)
+- **后端**: Node.js, Express.js
+- **实时通信**: Socket.IO
+- **音视频**: WebRTC API, Canvas API
+- **视频处理**: MediaStream API
+
+## 安装和运行
+
+### 前提条件
+
+- Node.js (版本 14 或更高)
+- 现代浏览器 (支持WebRTC和Canvas)
+
+### 安装依赖
+
+```bash
+# 使用npm安装依赖
+npm install
+
+# 或使用yarn安装依赖
+yarn install
+```
+
+### 启动应用
+
+```bash
+# 开发模式(自动重启)
+npm run dev
+
+# 或生产模式
+npm start
+```
+
+访问 `http://localhost:3000` 开始使用应用。
+
+## 项目结构
+
+```
+new_rtc/
+├── src/ # 前端源代码
+│ ├── index.html # 主HTML文件
+│ ├── index.js # 主JavaScript文件
+│ └── styles.css # 样式文件
+├── videos/ # 视频文件目录
+│ ├── asd.mp4 # 默认视频文件
+│ ├── zxc.mp4 # 示例视频文件
+│ └── jkl.mp4 # 示例视频文件
+├── server.js # Express服务器
+├── package.json # 项目配置
+└── README.md # 项目说明
+```
+
+## 使用说明
+
+### 1. 开始音频通话
+
+1. 点击"开始音频通话"按钮
+2. 允许浏览器访问麦克风
+3. 音频状态将显示"已连接"
+4. 系统自动开始播放默认视频流(9:16比例,无播放条)
+
+### 2. 智能视频流切换
+
+1. **WebRTC视频流**: 使用WebRTC技术传输视频流,而非播放本地文件
+2. **固定比例显示**: 视频播放框固定9:16比例,适合移动端观看
+3. **纯净界面**: 隐藏播放条,提供沉浸式观看体验
+4. **文本输入**: 在文本框中输入内容,系统会根据关键词自动切换视频流
+5. **语音输入**: 点击语音按钮进行语音输入,系统会识别并切换视频流
+6. **自动回退**: 交互结束后10秒自动回到默认视频流
+7. **手动控制**: 点击"回到默认视频"按钮立即回到默认视频流
+
+### 3. 视频映射配置
+
+在 `server.js` 中可以配置文本到视频的映射关系:
+
+```javascript
+const videoMapping = {
+ '你好': 'asd.mp4',
+ 'hello': 'asd.mp4',
+ '再见': 'zxc.mp4',
+ 'goodbye': 'zxc.mp4',
+ '谢谢': 'jkl.mp4',
+ 'thank you': 'jkl.mp4',
+ '默认': 'asd.mp4'
+};
+```
+
+### 4. 默认视频配置
+
+在 `server.js` 中可以修改默认视频:
+
+```javascript
+const DEFAULT_VIDEO = 'asd.mp4'; // 修改为您的默认视频文件名
+```
+
+## 添加新视频
+
+1. 将视频文件放入 `videos/` 目录
+2. 支持的格式: MP4, WebM, AVI
+3. 在 `server.js` 中更新视频映射配置
+4. 重启服务器
+
+## 语音识别
+
+当前版本使用模拟语音识别。要集成真实的语音识别,可以:
+
+1. 使用Web Speech API
+2. 集成第三方语音识别服务(如百度、阿里云等)
+3. 修改 `processVoiceInput` 方法中的语音识别逻辑
+
+## 浏览器兼容性
+
+- Chrome 60+
+- Firefox 55+
+- Safari 11+
+- Edge 79+
+
+## 注意事项
+
+1. 需要HTTPS环境才能使用麦克风(本地开发除外)
+2. 确保浏览器允许访问麦克风权限
+3. 视频文件应放在 `videos/` 目录中
+4. 建议使用现代浏览器以获得最佳体验
+5. 交互超时时间可在服务器端配置(默认10秒)
+6. 视频流使用Canvas渲染,确保浏览器支持Canvas API
+
+## 故障排除
+
+### 无法访问麦克风
+- 检查浏览器权限设置
+- 确保使用HTTPS或localhost
+- 检查设备是否被其他应用占用
+
+### 视频流无法创建或黑屏
+- 检查视频文件格式是否支持
+- 确认视频文件路径正确
+- 检查浏览器是否支持Canvas API
+- 查看浏览器控制台是否有错误信息
+- 使用"测试视频文件"按钮检查视频文件是否可访问
+- 确保视频文件在 `videos/` 目录中存在
+
+### 连接问题
+- 检查网络连接
+- 确认服务器正在运行
+- 检查防火墙设置
+
+### CSS样式问题
+- 确保 `src/styles.css` 文件存在
+- 检查浏览器控制台是否有404错误
+- 确认服务器正确配置了静态文件服务
+
+## 许可证
+
+MIT License
+
+## 贡献
+
+欢迎提交Issue和Pull Request来改进这个项目。
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0f44d8e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "webrtc-video-chat",
+ "version": "1.0.0",
+ "description": "基于WebRTC的音视频通话应用,支持实时播放录制视频",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "nodemon server.js"
+ },
+ "dependencies": {
+ "express": "^4.18.2",
+ "socket.io": "^4.7.2",
+ "cors": "^2.8.5"
+ },
+ "devDependencies": {
+ "nodemon": "^3.0.1"
+ },
+ "keywords": ["webrtc", "video", "chat", "realtime"],
+ "author": "Your Name",
+ "license": "MIT"
+}
\ No newline at end of file
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..6057d2b
--- /dev/null
+++ b/server.js
@@ -0,0 +1,224 @@
+const express = require('express');
+const http = require('http');
+const socketIo = require('socket.io');
+const cors = require('cors');
+const path = require('path');
+const fs = require('fs');
+
+const app = express();
+const server = http.createServer(app);
+const io = socketIo(server, {
+ cors: {
+ origin: "*",
+ methods: ["GET", "POST"]
+ }
+});
+
+// 中间件
+app.use(cors());
+app.use(express.json());
+app.use(express.static('src'));
+app.use('/videos', express.static('videos'));
+
+// 存储连接的客户端和他们的视频流状态
+const connectedClients = new Map();
+
+// 视频映射配置
+const videoMapping = {
+ '你好': 'asd.mp4',
+ 'hello': 'asd.mp4',
+ '再见': 'zxc.mp4',
+ 'goodbye': 'zxc.mp4',
+ '谢谢': 'jkl.mp4',
+ 'thank you': 'jkl.mp4',
+ '默认': 'asd.mp4'
+};
+
+// 默认视频流配置
+const DEFAULT_VIDEO = 'asd.mp4';
+const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频
+
+// 获取视频列表
+app.get('/api/videos', (req, res) => {
+ const videosDir = path.join(__dirname, 'videos');
+ fs.readdir(videosDir, (err, files) => {
+ if (err) {
+ return res.status(500).json({ error: '无法读取视频目录' });
+ }
+ const videoFiles = files.filter(file =>
+ file.endsWith('.mp4') || file.endsWith('.webm') || file.endsWith('.avi')
+ );
+ res.json({ videos: videoFiles });
+ });
+});
+
+// 获取视频映射
+app.get('/api/video-mapping', (req, res) => {
+ res.json({ mapping: videoMapping });
+});
+
+// 获取默认视频
+app.get('/api/default-video', (req, res) => {
+ res.json({
+ defaultVideo: DEFAULT_VIDEO,
+ autoLoop: true
+ });
+});
+
+// Socket.IO 连接处理
+io.on('connection', (socket) => {
+ console.log('用户连接:', socket.id);
+ connectedClients.set(socket.id, {
+ socket: socket,
+ currentVideo: DEFAULT_VIDEO,
+ isInInteraction: false
+ });
+
+ // 处理WebRTC信令 - 用于传输视频流
+ socket.on('offer', (data) => {
+ console.log('收到offer:', socket.id);
+ socket.broadcast.emit('offer', {
+ ...data,
+ from: socket.id
+ });
+ });
+
+ socket.on('answer', (data) => {
+ console.log('收到answer:', socket.id);
+ socket.broadcast.emit('answer', {
+ ...data,
+ from: socket.id
+ });
+ });
+
+ socket.on('ice-candidate', (data) => {
+ console.log('收到ice-candidate:', socket.id);
+ socket.broadcast.emit('ice-candidate', {
+ ...data,
+ from: socket.id
+ });
+ });
+
+ // 处理视频流切换请求
+ socket.on('switch-video-stream', (data) => {
+ const { videoFile, type, text } = data;
+ console.log(`用户 ${socket.id} 请求切换视频流: ${videoFile} (${type})`);
+
+ // 更新客户端状态
+ const client = connectedClients.get(socket.id);
+ if (client) {
+ client.currentVideo = videoFile;
+ client.isInInteraction = true;
+ }
+
+ // 广播视频流切换指令给所有用户
+ io.emit('video-stream-switched', {
+ videoFile,
+ type,
+ text,
+ from: socket.id
+ });
+
+ // 如果是交互类型,设置定时器回到默认视频
+ if (type === 'text' || type === 'voice') {
+ setTimeout(() => {
+ console.log(`交互超时,用户 ${socket.id} 回到默认视频`);
+ if (client) {
+ client.currentVideo = DEFAULT_VIDEO;
+ client.isInInteraction = false;
+ }
+ // 广播回到默认视频的指令
+ io.emit('video-stream-switched', {
+ videoFile: DEFAULT_VIDEO,
+ type: 'default',
+ from: socket.id
+ });
+ }, INTERACTION_TIMEOUT);
+ }
+ });
+
+ // 处理通话开始
+ socket.on('call-started', () => {
+ console.log('通话开始,用户:', socket.id);
+ const client = connectedClients.get(socket.id);
+ if (client) {
+ client.currentVideo = DEFAULT_VIDEO;
+ client.isInInteraction = false;
+ }
+ io.emit('call-started', { from: socket.id });
+ });
+
+ // 处理文本输入
+ socket.on('text-input', (data) => {
+ const { text } = data;
+ console.log('收到文本输入:', text, '来自用户:', socket.id);
+
+ // 根据文本查找对应视频
+ let videoFile = videoMapping['默认'];
+ for (const [key, value] of Object.entries(videoMapping)) {
+ if (text.toLowerCase().includes(key.toLowerCase())) {
+ videoFile = value;
+ break;
+ }
+ }
+
+ console.log(`用户 ${socket.id} 文本输入 "${text}" 对应视频: ${videoFile}`);
+
+ // 发送视频流切换请求
+ socket.emit('switch-video-stream', {
+ videoFile,
+ type: 'text',
+ text
+ });
+ });
+
+ // 处理语音输入
+ socket.on('voice-input', (data) => {
+ const { audioData, text } = data;
+ console.log('收到语音输入:', text, '来自用户:', socket.id);
+
+ // 根据语音识别的文本查找对应视频
+ let videoFile = videoMapping['默认'];
+ for (const [key, value] of Object.entries(videoMapping)) {
+ if (text.toLowerCase().includes(key.toLowerCase())) {
+ videoFile = value;
+ break;
+ }
+ }
+
+ console.log(`用户 ${socket.id} 语音输入 "${text}" 对应视频: ${videoFile}`);
+
+ // 发送视频流切换请求
+ socket.emit('switch-video-stream', {
+ videoFile,
+ type: 'voice',
+ text
+ });
+ });
+
+ // 处理回到默认视频请求
+ socket.on('return-to-default', () => {
+ console.log('用户请求回到默认视频:', socket.id);
+ const client = connectedClients.get(socket.id);
+ if (client) {
+ client.currentVideo = DEFAULT_VIDEO;
+ client.isInInteraction = false;
+ }
+ socket.emit('switch-video-stream', {
+ videoFile: DEFAULT_VIDEO,
+ type: 'default'
+ });
+ });
+
+ // 断开连接
+ socket.on('disconnect', () => {
+ console.log('用户断开连接:', socket.id);
+ connectedClients.delete(socket.id);
+ });
+});
+
+const PORT = process.env.PORT || 3000;
+server.listen(PORT, () => {
+ console.log(`服务器运行在端口 ${PORT}`);
+ console.log(`访问 http://localhost:${PORT} 开始使用`);
+});
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..b5f8049
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+ WebRTC 音频通话
+
+
+
+
+
+ WebRTC 音频通话
+ 实时播放录制视频,支持文本和语音输入
+
+
+
+
+
+
+
+
+
录制视频播放
+
+
+ 未选择视频
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..13a776d
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,767 @@
+// WebRTC 音视频通话应用
+class WebRTCChat {
+ constructor() {
+ this.socket = null;
+ this.localStream = null;
+ this.peerConnection = null;
+ this.isRecording = false;
+ this.mediaRecorder = null;
+ this.audioChunks = [];
+ this.videoMapping = {};
+ this.defaultVideo = 'asd.mp4';
+ this.currentVideo = null;
+ this.videoStreams = new Map(); // 存储不同视频的MediaStream
+ this.currentVideoStream = null;
+
+ this.initializeElements();
+ this.initializeSocket();
+ this.loadVideoMapping();
+ this.loadVideoList();
+ this.loadDefaultVideo();
+ this.bindEvents();
+ }
+
+ initializeElements() {
+ // 视频元素
+ this.localVideo = document.getElementById('localVideo');
+ this.remoteVideo = document.getElementById('remoteVideo');
+ this.recordedVideo = document.getElementById('recordedVideo');
+
+ // 音频状态元素
+ this.audioStatus = document.getElementById('audioStatus');
+
+ // 按钮元素
+ this.startButton = document.getElementById('startButton');
+ this.stopButton = document.getElementById('stopButton');
+ this.muteButton = document.getElementById('muteButton');
+ this.sendTextButton = document.getElementById('sendTextButton');
+ this.startVoiceButton = document.getElementById('startVoiceButton');
+ this.stopVoiceButton = document.getElementById('stopVoiceButton');
+ this.defaultVideoButton = document.getElementById('defaultVideoButton');
+ this.testVideoButton = document.getElementById('testVideoButton'); // 新增测试按钮
+
+ // 输入元素
+ this.textInput = document.getElementById('textInput');
+ this.voiceStatus = document.getElementById('voiceStatus');
+
+ // 状态元素
+ this.connectionStatus = document.getElementById('connectionStatus');
+ this.messageLog = document.getElementById('messageLog');
+ this.currentVideoName = document.getElementById('currentVideoName');
+ this.videoList = document.getElementById('videoList');
+ }
+
+ initializeSocket() {
+ this.socket = io();
+
+ this.socket.on('connect', () => {
+ this.updateStatus('已连接到服务器', 'connected');
+ this.logMessage('已连接到服务器', 'success');
+ });
+
+ this.socket.on('disconnect', () => {
+ this.updateStatus('与服务器断开连接', 'disconnected');
+ this.logMessage('与服务器断开连接', 'error');
+ });
+
+ // WebRTC 信令处理
+ this.socket.on('offer', (data) => {
+ this.handleOffer(data);
+ });
+
+ this.socket.on('answer', (data) => {
+ this.handleAnswer(data);
+ });
+
+ this.socket.on('ice-candidate', (data) => {
+ this.handleIceCandidate(data);
+ });
+
+ // 视频流切换处理
+ this.socket.on('video-stream-switched', (data) => {
+ this.logMessage(`收到视频流切换指令: ${data.videoFile} (${data.type}) 来自用户: ${data.from}`, 'info');
+ this.switchVideoStream(data.videoFile, data.type, data.text);
+ });
+
+ // 通话开始处理
+ this.socket.on('call-started', (data) => {
+ this.logMessage('通话已开始', 'success');
+ this.startDefaultVideoStream();
+ });
+ }
+
+ async loadVideoMapping() {
+ try {
+ const response = await fetch('/api/video-mapping');
+ const data = await response.json();
+ this.videoMapping = data.mapping;
+ this.logMessage('视频映射加载成功', 'success');
+ } catch (error) {
+ this.logMessage('加载视频映射失败: ' + error.message, 'error');
+ }
+ }
+
+ async loadDefaultVideo() {
+ try {
+ const response = await fetch('/api/default-video');
+ const data = await response.json();
+ this.defaultVideo = data.defaultVideo;
+ this.logMessage('默认视频配置加载成功', 'success');
+ } catch (error) {
+ this.logMessage('加载默认视频配置失败: ' + error.message, 'error');
+ }
+ }
+
+ async loadVideoList() {
+ try {
+ const response = await fetch('/api/videos');
+ const data = await response.json();
+ this.renderVideoList(data.videos);
+ this.logMessage('视频列表加载成功', 'success');
+ } catch (error) {
+ this.logMessage('加载视频列表失败: ' + error.message, 'error');
+ }
+ }
+
+ renderVideoList(videos) {
+ this.videoList.innerHTML = '';
+ videos.forEach(video => {
+ const videoItem = document.createElement('div');
+ videoItem.className = 'video-item';
+ videoItem.textContent = video;
+ videoItem.onclick = () => this.selectVideo(video);
+ this.videoList.appendChild(videoItem);
+ });
+ }
+
+ selectVideo(videoFile) {
+ // 移除之前的active类
+ document.querySelectorAll('.video-item').forEach(item => {
+ item.classList.remove('active');
+ });
+
+ // 添加active类到选中的视频
+ event.target.classList.add('active');
+
+ // 切换到选中的视频流
+ this.switchVideoStream(videoFile, 'manual');
+
+ // 通知服务器切换视频流
+ this.socket.emit('switch-video-stream', {
+ videoFile,
+ type: 'manual'
+ });
+ }
+
+ async startDefaultVideoStream() {
+ try {
+ this.logMessage('开始创建默认视频流', 'info');
+
+ // 添加加载状态
+ this.recordedVideo.classList.add('loading');
+
+ // 创建默认视频的MediaStream
+ const defaultStream = await this.createVideoStream(this.defaultVideo);
+
+ // 等待流稳定
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // 检查流是否有效
+ if (!defaultStream || defaultStream.getTracks().length === 0) {
+ throw new Error('默认视频流创建失败');
+ }
+
+ // 设置视频流
+ this.currentVideoStream = defaultStream;
+ this.recordedVideo.srcObject = defaultStream;
+ this.currentVideo = this.defaultVideo;
+ this.currentVideoName.textContent = `默认视频: ${this.defaultVideo}`;
+
+ // 等待视频元素准备就绪
+ await new Promise(resolve => {
+ const checkReady = () => {
+ if (this.recordedVideo.readyState >= 2) { // HAVE_CURRENT_DATA
+ resolve();
+ } else {
+ setTimeout(checkReady, 100);
+ }
+ };
+ checkReady();
+ });
+
+ // 确保视频开始播放
+ try {
+ await this.recordedVideo.play();
+ this.logMessage('默认视频开始播放', 'success');
+
+ // 移除加载状态,添加播放状态
+ this.recordedVideo.classList.remove('loading');
+ this.recordedVideo.classList.add('playing');
+ } catch (playError) {
+ this.logMessage(`默认视频播放失败: ${playError.message}`, 'error');
+ this.recordedVideo.classList.remove('loading');
+ }
+
+ this.logMessage('默认视频流创建成功', 'success');
+ } catch (error) {
+ this.logMessage('创建默认视频流失败: ' + error.message, 'error');
+ this.recordedVideo.classList.remove('loading');
+ }
+ }
+
+ async testVideoFile(videoFile) {
+ return new Promise((resolve, reject) => {
+ const testVideo = document.createElement('video');
+ testVideo.src = `/videos/${videoFile}`;
+ testVideo.muted = true;
+
+ testVideo.onloadedmetadata = () => {
+ this.logMessage(`视频文件测试成功: ${videoFile} (${testVideo.videoWidth}x${testVideo.videoHeight})`, 'success');
+ resolve(true);
+ };
+
+ testVideo.onerror = () => {
+ this.logMessage(`视频文件测试失败: ${videoFile}`, 'error');
+ reject(new Error(`视频文件不存在或无法加载: ${videoFile}`));
+ };
+
+ // 设置超时
+ setTimeout(() => {
+ reject(new Error(`视频文件加载超时: ${videoFile}`));
+ }, 10000);
+ });
+ }
+
+ async createVideoStream(videoFile) {
+ // 如果已经缓存了这个视频流,直接返回
+ if (this.videoStreams.has(videoFile)) {
+ this.logMessage(`使用缓存的视频流: ${videoFile}`, 'info');
+ return this.videoStreams.get(videoFile);
+ }
+
+ try {
+ this.logMessage(`开始创建视频流: ${videoFile}`, 'info');
+
+ // 先测试视频文件是否存在
+ await this.testVideoFile(videoFile);
+
+ // 创建video元素来加载视频
+ const video = document.createElement('video');
+ video.src = `/videos/${videoFile}`;
+ video.muted = true;
+ video.loop = true;
+ video.autoplay = true;
+ video.crossOrigin = 'anonymous'; // 添加跨域支持
+ video.playsInline = true; // 添加playsInline属性
+
+ // 等待视频加载完成
+ await new Promise((resolve, reject) => {
+ video.onloadedmetadata = () => {
+ this.logMessage(`视频元数据加载完成: ${videoFile}`, 'info');
+ resolve();
+ };
+ video.onerror = (error) => {
+ this.logMessage(`视频加载失败: ${videoFile}`, 'error');
+ reject(error);
+ };
+ video.onloadstart = () => {
+ this.logMessage(`开始加载视频: ${videoFile}`, 'info');
+ };
+ video.oncanplay = () => {
+ this.logMessage(`视频可以播放: ${videoFile}`, 'info');
+ };
+ });
+
+ // 创建MediaStream
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+
+ // 设置canvas尺寸为视频尺寸
+ canvas.width = video.videoWidth || 640;
+ canvas.height = video.videoHeight || 480;
+
+ this.logMessage(`Canvas尺寸: ${canvas.width}x${canvas.height}`, 'info');
+
+ // 开始播放视频
+ try {
+ await video.play();
+ this.logMessage(`视频开始播放: ${videoFile}`, 'info');
+ } catch (playError) {
+ this.logMessage(`视频播放失败: ${playError.message}`, 'error');
+ // 即使播放失败也继续创建流
+ }
+
+ // 等待视频开始播放
+ await new Promise(resolve => {
+ const checkPlay = () => {
+ if (video.readyState >= video.HAVE_CURRENT_DATA) {
+ resolve();
+ } else {
+ setTimeout(checkPlay, 100);
+ }
+ };
+ checkPlay();
+ });
+
+ // 绘制视频到canvas
+ let isDrawing = false;
+ const drawFrame = () => {
+ if (video.readyState >= video.HAVE_CURRENT_DATA && !isDrawing) {
+ isDrawing = true;
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+ isDrawing = false;
+ }
+ requestAnimationFrame(drawFrame);
+ };
+
+ // 开始绘制帧
+ drawFrame();
+
+ // 从canvas创建MediaStream
+ const stream = canvas.captureStream(30); // 30fps
+
+ // 等待流创建完成并稳定
+ await new Promise(resolve => {
+ setTimeout(resolve, 500); // 给更多时间让流稳定
+ });
+
+ this.logMessage(`视频流创建成功: ${videoFile}`, 'success');
+
+ // 缓存这个视频流
+ this.videoStreams.set(videoFile, stream);
+
+ return stream;
+ } catch (error) {
+ this.logMessage(`创建视频流失败 ${videoFile}: ${error.message}`, 'error');
+ throw error;
+ }
+ }
+
+ async switchVideoStream(videoFile, type = '', text = '') {
+ try {
+ this.logMessage(`开始切换视频流: ${videoFile} (${type})`, 'info');
+
+ // 添加加载状态
+ this.recordedVideo.classList.add('loading');
+
+ // 先创建新的视频流,不立即停止旧的
+ const newStream = await this.createVideoStream(videoFile);
+
+ // 等待流稳定
+ await new Promise(resolve => setTimeout(resolve, 200));
+
+ // 检查流是否有效
+ if (!newStream || newStream.getTracks().length === 0) {
+ throw new Error('创建的视频流无效');
+ }
+
+ // 先设置新的视频流,再停止旧的
+ this.currentVideoStream = newStream;
+ this.recordedVideo.srcObject = newStream;
+ this.currentVideo = videoFile;
+
+ // 确保视频开始播放
+ try {
+ await this.recordedVideo.play();
+ this.logMessage('视频元素开始播放', 'info');
+
+ // 移除加载状态,添加播放状态
+ this.recordedVideo.classList.remove('loading');
+ this.recordedVideo.classList.add('playing');
+ } catch (playError) {
+ this.logMessage(`视频播放失败: ${playError.message}`, 'error');
+ this.recordedVideo.classList.remove('loading');
+ }
+
+ // 现在停止旧的视频流
+ if (this.currentVideoStream !== newStream) {
+ const oldStream = this.currentVideoStream;
+ setTimeout(() => {
+ if (oldStream) {
+ oldStream.getTracks().forEach(track => {
+ track.stop();
+ this.logMessage(`已停止旧轨道: ${track.kind}`, 'info');
+ });
+ }
+ }, 1000); // 延迟1秒停止旧流,确保新流已经稳定
+ }
+
+ if (text) {
+ this.currentVideoName.textContent = `交互视频: ${videoFile} (${type}: ${text})`;
+ this.logMessage(`成功切换到交互视频流: ${videoFile} (${type}: ${text})`, 'success');
+ } else {
+ this.currentVideoName.textContent = `视频流: ${videoFile}`;
+ this.logMessage(`成功切换到视频流: ${videoFile}`, 'success');
+ }
+
+ // 检查切换后的状态
+ setTimeout(() => {
+ this.checkVideoStreamStatus();
+ }, 1000);
+
+ } catch (error) {
+ this.logMessage(`切换视频流失败: ${error.message}`, 'error');
+ this.recordedVideo.classList.remove('loading');
+
+ // 如果切换失败,尝试回到默认视频
+ if (videoFile !== this.defaultVideo) {
+ this.logMessage('尝试回到默认视频', 'info');
+ await this.switchVideoStream(this.defaultVideo, 'fallback');
+ }
+ }
+ }
+
+ bindEvents() {
+ // 开始通话按钮
+ this.startButton.onclick = () => this.startCall();
+
+ // 停止通话按钮
+ this.stopButton.onclick = () => this.stopCall();
+
+ // 静音按钮
+ this.muteButton.onclick = () => this.toggleMute();
+
+ // 回到默认视频按钮
+ this.defaultVideoButton.onclick = () => this.returnToDefaultVideo();
+
+ // 测试视频文件按钮
+ this.testVideoButton.onclick = () => this.testAllVideoFiles();
+
+ // 发送文本按钮
+ this.sendTextButton.onclick = () => this.sendText();
+
+ // 回车键发送文本
+ this.textInput.onkeypress = (e) => {
+ if (e.key === 'Enter') {
+ this.sendText();
+ }
+ };
+
+ // 语音输入按钮
+ this.startVoiceButton.onclick = () => this.startVoiceRecording();
+ this.stopVoiceButton.onclick = () => this.stopVoiceRecording();
+ }
+
+ async startCall() {
+ try {
+ this.localStream = await navigator.mediaDevices.getUserMedia({
+ video: false,
+ audio: true
+ });
+
+ this.createPeerConnection();
+
+ this.startButton.disabled = true;
+ this.stopButton.disabled = false;
+
+ this.updateAudioStatus('已连接', 'connected');
+ this.logMessage('音频通话已开始', 'success');
+
+ // 确保视频映射已加载
+ if (Object.keys(this.videoMapping).length === 0) {
+ await this.loadVideoMapping();
+ }
+
+ this.logMessage(`视频映射已加载: ${Object.keys(this.videoMapping).length} 个映射`, 'info');
+
+ // 通知服务器通话开始
+ this.socket.emit('call-started');
+
+ } catch (error) {
+ this.logMessage('无法访问麦克风: ' + error.message, 'error');
+ }
+ }
+
+ stopCall() {
+ if (this.localStream) {
+ this.localStream.getTracks().forEach(track => track.stop());
+ this.localStream = null;
+ }
+
+ if (this.peerConnection) {
+ this.peerConnection.close();
+ this.peerConnection = null;
+ }
+
+ // 停止当前视频流
+ if (this.currentVideoStream) {
+ this.currentVideoStream.getTracks().forEach(track => track.stop());
+ this.currentVideoStream = null;
+ }
+
+ this.recordedVideo.srcObject = null;
+ this.currentVideo = null;
+ this.currentVideoName.textContent = '未选择视频';
+
+ this.startButton.disabled = false;
+ this.stopButton.disabled = true;
+
+ this.updateAudioStatus('未连接', 'disconnected');
+ this.logMessage('音频通话已结束', 'info');
+ }
+
+ createPeerConnection() {
+ const configuration = {
+ iceServers: [
+ { urls: "stun:stun.qq.com:3478" },
+ { urls: "stun:stun.miwifi.com:3478" },
+ { urls: 'stun:stun.l.google.com:19302' },
+ { urls: 'stun:stun1.l.google.com:19302' }
+ ]
+ };
+
+ this.peerConnection = new RTCPeerConnection(configuration);
+
+ // 添加本地音频流
+ this.localStream.getTracks().forEach(track => {
+ this.peerConnection.addTrack(track, this.localStream);
+ });
+
+ // 处理远程流
+ this.peerConnection.ontrack = (event) => {
+ this.remoteVideo.srcObject = event.streams[0];
+ };
+
+ // 处理ICE候选
+ this.peerConnection.onicecandidate = (event) => {
+ if (event.candidate) {
+ this.socket.emit('ice-candidate', {
+ candidate: event.candidate
+ });
+ }
+ };
+
+ // 创建并发送offer
+ this.peerConnection.createOffer()
+ .then(offer => this.peerConnection.setLocalDescription(offer))
+ .then(() => {
+ this.socket.emit('offer', {
+ offer: this.peerConnection.localDescription
+ });
+ })
+ .catch(error => {
+ this.logMessage('创建offer失败: ' + error.message, 'error');
+ });
+ }
+
+ async handleOffer(data) {
+ if (!this.peerConnection) {
+ await this.startCall();
+ }
+
+ await this.peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
+
+ const answer = await this.peerConnection.createAnswer();
+ await this.peerConnection.setLocalDescription(answer);
+
+ this.socket.emit('answer', {
+ answer: this.peerConnection.localDescription
+ });
+ }
+
+ async handleAnswer(data) {
+ if (this.peerConnection) {
+ await this.peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
+ }
+ }
+
+ async handleIceCandidate(data) {
+ if (this.peerConnection) {
+ await this.peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
+ }
+ }
+
+ toggleMute() {
+ if (this.localStream) {
+ const audioTrack = this.localStream.getAudioTracks()[0];
+ if (audioTrack) {
+ audioTrack.enabled = !audioTrack.enabled;
+ this.muteButton.textContent = audioTrack.enabled ? '静音' : '取消静音';
+ this.logMessage(audioTrack.enabled ? '已取消静音' : '已静音', 'info');
+ }
+ }
+ }
+
+ sendText() {
+ const text = this.textInput.value.trim();
+ if (text) {
+ this.socket.emit('text-input', { text });
+ this.logMessage(`发送文本: ${text}`, 'info');
+ this.textInput.value = '';
+
+ // 根据文本查找对应视频并切换
+ this.handleTextInput(text);
+ }
+ }
+
+ async handleTextInput(text) {
+ // 根据文本查找对应视频
+ let videoFile = this.videoMapping['默认'] || this.defaultVideo;
+ for (const [key, value] of Object.entries(this.videoMapping)) {
+ if (text.toLowerCase().includes(key.toLowerCase())) {
+ videoFile = value;
+ break;
+ }
+ }
+
+ // 切换到对应的视频流
+ await this.switchVideoStream(videoFile, 'text', text);
+
+ // 通知服务器切换视频流
+ this.socket.emit('switch-video-stream', {
+ videoFile,
+ type: 'text',
+ text
+ });
+ }
+
+ async startVoiceRecording() {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ this.mediaRecorder = new MediaRecorder(stream);
+ this.audioChunks = [];
+
+ this.mediaRecorder.ondataavailable = (event) => {
+ this.audioChunks.push(event.data);
+ };
+
+ this.mediaRecorder.onstop = () => {
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
+ this.processVoiceInput(audioBlob);
+ };
+
+ this.mediaRecorder.start();
+ this.isRecording = true;
+
+ this.startVoiceButton.disabled = true;
+ this.stopVoiceButton.disabled = false;
+ this.voiceStatus.textContent = '正在录音...';
+ this.startVoiceButton.classList.add('recording');
+
+ this.logMessage('开始语音录制', 'info');
+ } catch (error) {
+ this.logMessage('无法访问麦克风: ' + error.message, 'error');
+ }
+ }
+
+ stopVoiceRecording() {
+ if (this.mediaRecorder && this.isRecording) {
+ this.mediaRecorder.stop();
+ this.isRecording = false;
+
+ this.startVoiceButton.disabled = false;
+ this.stopVoiceButton.disabled = true;
+ this.voiceStatus.textContent = '点击开始语音输入';
+ this.startVoiceButton.classList.remove('recording');
+
+ this.logMessage('停止语音录制', 'info');
+ }
+ }
+
+ async processVoiceInput(audioBlob) {
+ // 这里可以集成语音识别API,如Web Speech API或第三方服务
+ // 为了演示,我们使用一个简单的模拟识别
+ const mockText = this.simulateSpeechRecognition();
+
+ this.socket.emit('voice-input', {
+ audioData: audioBlob,
+ text: mockText
+ });
+
+ this.logMessage(`语音识别结果: ${mockText}`, 'info');
+
+ // 根据语音识别结果切换视频流
+ await this.handleVoiceInput(mockText);
+ }
+
+ async handleVoiceInput(text) {
+ // 根据文本查找对应视频
+ let videoFile = this.videoMapping['默认'] || this.defaultVideo;
+ for (const [key, value] of Object.entries(this.videoMapping)) {
+ if (text.toLowerCase().includes(key.toLowerCase())) {
+ videoFile = value;
+ break;
+ }
+ }
+
+ // 切换到对应的视频流
+ await this.switchVideoStream(videoFile, 'voice', text);
+
+ // 通知服务器切换视频流
+ this.socket.emit('switch-video-stream', {
+ videoFile,
+ type: 'voice',
+ text
+ });
+ }
+
+ simulateSpeechRecognition() {
+ // 模拟语音识别,随机返回预设的文本
+ const texts = ['你好', '再见', '谢谢', 'hello', 'goodbye', 'thank you'];
+ return texts[Math.floor(Math.random() * texts.length)];
+ }
+
+ returnToDefaultVideo() {
+ this.switchVideoStream(this.defaultVideo, 'default');
+ this.socket.emit('return-to-default');
+ this.logMessage(`已返回至默认视频: ${this.defaultVideo}`, 'info');
+ }
+
+ updateStatus(message, type) {
+ this.connectionStatus.textContent = message;
+ this.connectionStatus.className = `status ${type}`;
+ }
+
+ updateAudioStatus(message, type) {
+ this.audioStatus.textContent = message;
+ this.audioStatus.className = `status-indicator ${type}`;
+ }
+
+ logMessage(message, type = 'info') {
+ const logEntry = document.createElement('div');
+ logEntry.className = type;
+ logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
+
+ this.messageLog.appendChild(logEntry);
+ this.messageLog.scrollTop = this.messageLog.scrollHeight;
+ }
+
+ checkVideoStreamStatus() {
+ const status = {
+ hasStream: !!this.currentVideoStream,
+ streamTracks: this.currentVideoStream ? this.currentVideoStream.getTracks().length : 0,
+ videoReadyState: this.recordedVideo.readyState,
+ videoPaused: this.recordedVideo.paused,
+ videoCurrentTime: this.recordedVideo.currentTime,
+ videoDuration: this.recordedVideo.duration,
+ currentVideo: this.currentVideo
+ };
+
+ this.logMessage(`视频流状态: ${JSON.stringify(status)}`, 'info');
+ return status;
+ }
+
+ async testAllVideoFiles() {
+ this.logMessage('开始测试所有视频文件...', 'info');
+
+ const videoFiles = ['asd.mp4', 'zxc.mp4', 'jkl.mp4'];
+
+ for (const videoFile of videoFiles) {
+ try {
+ await this.testVideoFile(videoFile);
+ } catch (error) {
+ this.logMessage(`视频文件 ${videoFile} 测试失败: ${error.message}`, 'error');
+ }
+ }
+
+ this.logMessage('视频文件测试完成', 'info');
+
+ // 检查当前视频流状态
+ this.checkVideoStreamStatus();
+ }
+}
+
+// 页面加载完成后初始化应用
+document.addEventListener('DOMContentLoaded', () => {
+ new WebRTCChat();
+});
\ No newline at end of file
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..e2b65a8
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,437 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ min-height: 100vh;
+ color: #333;
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+header {
+ text-align: center;
+ margin-bottom: 30px;
+ color: white;
+}
+
+header h1 {
+ font-size: 2.5rem;
+ margin-bottom: 10px;
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
+}
+
+header p {
+ font-size: 1.1rem;
+ opacity: 0.9;
+}
+
+.main-content {
+ background: white;
+ border-radius: 15px;
+ padding: 30px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
+}
+
+/* 视频区域 */
+.video-section {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.video-container {
+ position: relative;
+ background: #000;
+ border-radius: 10px;
+ overflow: hidden;
+ aspect-ratio: 16/9;
+}
+
+.video-container video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.video-label {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background: rgba(0,0,0,0.7);
+ color: white;
+ padding: 5px 10px;
+ border-radius: 5px;
+ font-size: 0.9rem;
+}
+
+/* 音频状态显示 */
+.audio-status {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 10px;
+ color: white;
+}
+
+.status-indicator {
+ font-size: 1.2rem;
+ font-weight: bold;
+}
+
+.status-indicator.connected {
+ color: #90EE90;
+}
+
+.status-indicator.disconnected {
+ color: #FFB6C1;
+}
+
+/* 录制视频区域 */
+.recorded-video-section {
+ margin-bottom: 30px;
+ text-align: center;
+}
+
+.recorded-video-section h3 {
+ margin-bottom: 15px;
+ color: #333;
+}
+
+#recordedVideo {
+ width: 100%;
+ max-width: 400px; /* 限制最大宽度 */
+ aspect-ratio: 9/16; /* 固定9:16比例 */
+ border-radius: 10px;
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
+ object-fit: cover; /* 确保视频填充容器 */
+ background: #000; /* 视频背景色 */
+ transition: opacity 0.3s ease; /* 添加透明度过渡效果 */
+}
+
+/* 视频加载时的样式 */
+#recordedVideo.loading {
+ opacity: 0.5;
+}
+
+/* 视频播放时的样式 */
+#recordedVideo.playing {
+ opacity: 1;
+}
+
+/* 隐藏视频播放条 */
+#recordedVideo::-webkit-media-controls {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-panel {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-play-button {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-timeline {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-current-time-display {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-time-remaining-display {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-mute-button {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-volume-slider {
+ display: none !important;
+}
+
+#recordedVideo::-webkit-media-controls-fullscreen-button {
+ display: none !important;
+}
+
+/* Firefox 隐藏播放条 */
+#recordedVideo::-moz-media-controls {
+ display: none !important;
+}
+
+/* 通用隐藏播放条 */
+#recordedVideo {
+ pointer-events: none; /* 禁用鼠标事件,防止用户点击播放条 */
+}
+
+.video-info {
+ margin-top: 10px;
+ font-weight: bold;
+ color: #666;
+}
+
+/* 控制按钮 */
+.controls {
+ display: flex;
+ gap: 15px;
+ justify-content: center;
+ margin-bottom: 30px;
+ flex-wrap: wrap;
+}
+
+.btn {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background: #007bff;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background: #0056b3;
+ transform: translateY(-2px);
+}
+
+.btn-danger {
+ background: #dc3545;
+ color: white;
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: #c82333;
+ transform: translateY(-2px);
+}
+
+.btn-secondary {
+ background: #6c757d;
+ color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: #545b62;
+ transform: translateY(-2px);
+}
+
+.btn-success {
+ background: #28a745;
+ color: white;
+}
+
+.btn-success:hover:not(:disabled) {
+ background: #1e7e34;
+ transform: translateY(-2px);
+}
+
+.btn-warning {
+ background: #ffc107;
+ color: #212529;
+}
+
+.btn-warning:hover:not(:disabled) {
+ background: #e0a800;
+ transform: translateY(-2px);
+}
+
+.btn-info {
+ background: #17a2b8;
+ color: white;
+}
+
+.btn-info:hover:not(:disabled) {
+ background: #138496;
+ transform: translateY(-2px);
+}
+
+/* 输入区域 */
+.input-section {
+ margin-bottom: 30px;
+}
+
+.text-input-group {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+#textInput {
+ flex: 1;
+ padding: 12px;
+ border: 2px solid #ddd;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: border-color 0.3s ease;
+}
+
+#textInput:focus {
+ outline: none;
+ border-color: #007bff;
+}
+
+.voice-input-group {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ flex-wrap: wrap;
+}
+
+#voiceStatus {
+ color: #666;
+ font-style: italic;
+}
+
+/* 视频选择 */
+.video-selection {
+ margin-bottom: 30px;
+}
+
+.video-selection h3 {
+ margin-bottom: 15px;
+ color: #333;
+}
+
+.video-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 15px;
+}
+
+.video-item {
+ background: #f8f9fa;
+ border: 2px solid #e9ecef;
+ border-radius: 8px;
+ padding: 15px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ text-align: center;
+}
+
+.video-item:hover {
+ border-color: #007bff;
+ background: #e3f2fd;
+ transform: translateY(-2px);
+}
+
+.video-item.active {
+ border-color: #007bff;
+ background: #007bff;
+ color: white;
+}
+
+/* 状态显示 */
+.status-section {
+ border-top: 2px solid #eee;
+ padding-top: 20px;
+}
+
+.status {
+ padding: 10px;
+ border-radius: 5px;
+ margin-bottom: 15px;
+ font-weight: bold;
+}
+
+.status.connected {
+ background: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.status.disconnected {
+ background: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+.message-log {
+ max-height: 200px;
+ overflow-y: auto;
+ background: #f8f9fa;
+ border-radius: 8px;
+ padding: 15px;
+ font-family: monospace;
+ font-size: 0.9rem;
+ border: 1px solid #e9ecef;
+}
+
+.message-log div {
+ margin-bottom: 5px;
+ padding: 5px;
+ border-radius: 3px;
+}
+
+.message-log .info {
+ background: #d1ecf1;
+ color: #0c5460;
+}
+
+.message-log .error {
+ background: #f8d7da;
+ color: #721c24;
+}
+
+.message-log .success {
+ background: #d4edda;
+ color: #155724;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .video-section {
+ grid-template-columns: 1fr;
+ }
+
+ .controls {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .text-input-group {
+ flex-direction: column;
+ }
+
+ .voice-input-group {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .video-list {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* 动画效果 */
+@keyframes pulse {
+ 0% { transform: scale(1); }
+ 50% { transform: scale(1.05); }
+ 100% { transform: scale(1); }
+}
+
+.recording {
+ animation: pulse 1s infinite;
+}
\ No newline at end of file
diff --git a/videos/asd.mp4 b/videos/asd.mp4
new file mode 100644
index 0000000..ec00ec5
Binary files /dev/null and b/videos/asd.mp4 differ
diff --git a/videos/jkl.mp4 b/videos/jkl.mp4
new file mode 100644
index 0000000..747cefe
Binary files /dev/null and b/videos/jkl.mp4 differ
diff --git a/videos/zxc.mp4 b/videos/zxc.mp4
new file mode 100644
index 0000000..8b2d55f
Binary files /dev/null and b/videos/zxc.mp4 differ
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..a547c1a
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,802 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
+ integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==
+
+"@types/cors@^2.8.12":
+ version "2.8.19"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.19.tgz#d93ea2673fd8c9f697367f5eeefc2bbfa94f0342"
+ integrity sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/node@*", "@types/node@>=10.0.0":
+ version "24.1.0"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-24.1.0.tgz#0993f7dc31ab5cc402d112315b463e383d68a49c"
+ integrity sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==
+ dependencies:
+ undici-types "~7.8.0"
+
+accepts@~1.3.4, accepts@~1.3.8:
+ version "1.3.8"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
+ integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
+ dependencies:
+ mime-types "~2.1.34"
+ negotiator "0.6.3"
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+base64id@2.0.0, base64id@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
+ integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
+
+binary-extensions@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
+ integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
+
+body-parser@1.20.3:
+ version "1.20.3"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
+ integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
+ dependencies:
+ bytes "3.1.2"
+ content-type "~1.0.5"
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ on-finished "2.4.1"
+ qs "6.13.0"
+ raw-body "2.5.2"
+ type-is "~1.6.18"
+ unpipe "1.0.0"
+
+brace-expansion@^1.1.7:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+bytes@3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
+ integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+call-bound@^1.0.2:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
+ integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ get-intrinsic "^1.3.0"
+
+chokidar@^3.5.2:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+content-disposition@0.5.4:
+ version "0.5.4"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
+ integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
+ dependencies:
+ safe-buffer "5.2.1"
+
+content-type@~1.0.4, content-type@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
+ integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+
+cookie@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
+ integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
+
+cookie@~0.7.2:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
+
+cors@^2.8.5, cors@~2.8.5:
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+ integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ dependencies:
+ object-assign "^4"
+ vary "^1"
+
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^4:
+ version "4.4.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b"
+ integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
+ dependencies:
+ ms "^2.1.3"
+
+debug@~4.3.1, debug@~4.3.2, debug@~4.3.4:
+ version "4.3.7"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52"
+ integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
+ dependencies:
+ ms "^2.1.3"
+
+depd@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+ integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
+ integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
+
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
+
+encodeurl@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
+
+encodeurl@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
+ integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
+
+engine.io-parser@~5.2.1:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f"
+ integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==
+
+engine.io@~6.6.0:
+ version "6.6.4"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.6.4.tgz#0a89a3e6b6c1d4b0c2a2a637495e7c149ec8d8ee"
+ integrity sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==
+ dependencies:
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
+ accepts "~1.3.4"
+ base64id "2.0.0"
+ cookie "~0.7.2"
+ cors "~2.8.5"
+ debug "~4.3.1"
+ engine.io-parser "~5.2.1"
+ ws "~8.17.1"
+
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
+
+express@^4.18.2:
+ version "4.21.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32"
+ integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==
+ dependencies:
+ accepts "~1.3.8"
+ array-flatten "1.1.1"
+ body-parser "1.20.3"
+ content-disposition "0.5.4"
+ content-type "~1.0.4"
+ cookie "0.7.1"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "2.0.0"
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "1.3.1"
+ fresh "0.5.2"
+ http-errors "2.0.0"
+ merge-descriptors "1.0.3"
+ methods "~1.1.2"
+ on-finished "2.4.1"
+ parseurl "~1.3.3"
+ path-to-regexp "0.1.12"
+ proxy-addr "~2.0.7"
+ qs "6.13.0"
+ range-parser "~1.2.1"
+ safe-buffer "5.2.1"
+ send "0.19.0"
+ serve-static "1.16.2"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ type-is "~1.6.18"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+finalhandler@1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
+ integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ on-finished "2.4.1"
+ parseurl "~1.3.3"
+ statuses "2.0.1"
+ unpipe "~1.0.0"
+
+forwarded@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
+ integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
+
+fsevents@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ function-bind "^1.1.2"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+http-errors@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
+ integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
+ dependencies:
+ depd "2.0.0"
+ inherits "2.0.4"
+ setprototypeof "1.2.0"
+ statuses "2.0.1"
+ toidentifier "1.0.1"
+
+iconv-lite@0.4.24:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ignore-by-default@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
+ integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
+
+inherits@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+ipaddr.js@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+ integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
+
+merge-descriptors@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
+ integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@~2.1.24, mime-types@~2.1.34:
+ version "2.1.35"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+mime@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+ integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+
+minimatch@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
+
+ms@2.1.3, ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+negotiator@0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
+ integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
+
+nodemon@^3.0.1:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1"
+ integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==
+ dependencies:
+ chokidar "^3.5.2"
+ debug "^4"
+ ignore-by-default "^1.0.1"
+ minimatch "^3.1.2"
+ pstree.remy "^1.1.8"
+ semver "^7.5.3"
+ simple-update-notifier "^2.0.0"
+ supports-color "^5.5.0"
+ touch "^3.1.0"
+ undefsafe "^2.0.5"
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+object-assign@^4:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-inspect@^1.13.3:
+ version "1.13.4"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
+ integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
+
+on-finished@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
+ integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
+ dependencies:
+ ee-first "1.1.1"
+
+parseurl@~1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
+ integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+
+path-to-regexp@0.1.12:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
+ integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+proxy-addr@~2.0.7:
+ version "2.0.7"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
+ integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+ dependencies:
+ forwarded "0.2.0"
+ ipaddr.js "1.9.1"
+
+pstree.remy@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
+ integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
+
+qs@6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
+ integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
+ dependencies:
+ side-channel "^1.0.6"
+
+range-parser@~1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
+ integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
+ dependencies:
+ bytes "3.1.2"
+ http-errors "2.0.0"
+ iconv-lite "0.4.24"
+ unpipe "1.0.0"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+safe-buffer@5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+semver@^7.5.3:
+ version "7.7.2"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
+send@0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
+ integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
+ dependencies:
+ debug "2.6.9"
+ depd "2.0.0"
+ destroy "1.2.0"
+ encodeurl "~1.0.2"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "2.0.0"
+ mime "1.6.0"
+ ms "2.1.3"
+ on-finished "2.4.1"
+ range-parser "~1.2.1"
+ statuses "2.0.1"
+
+serve-static@1.16.2:
+ version "1.16.2"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
+ integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
+ dependencies:
+ encodeurl "~2.0.0"
+ escape-html "~1.0.3"
+ parseurl "~1.3.3"
+ send "0.19.0"
+
+setprototypeof@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
+ integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
+
+side-channel-list@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
+ integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.3"
+
+side-channel-map@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
+ integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+
+side-channel-weakmap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
+ integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
+ dependencies:
+ call-bound "^1.0.2"
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.5"
+ object-inspect "^1.13.3"
+ side-channel-map "^1.0.1"
+
+side-channel@^1.0.6:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
+ integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
+ dependencies:
+ es-errors "^1.3.0"
+ object-inspect "^1.13.3"
+ side-channel-list "^1.0.0"
+ side-channel-map "^1.0.1"
+ side-channel-weakmap "^1.0.2"
+
+simple-update-notifier@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb"
+ integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==
+ dependencies:
+ semver "^7.5.3"
+
+socket.io-adapter@~2.5.2:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz#c7a1f9c703d7756844751b6ff9abfc1780664082"
+ integrity sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==
+ dependencies:
+ debug "~4.3.4"
+ ws "~8.17.1"
+
+socket.io-parser@~4.2.4:
+ version "4.2.4"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83"
+ integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==
+ dependencies:
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
+
+socket.io@^4.7.2:
+ version "4.8.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.8.1.tgz#fa0eaff965cc97fdf4245e8d4794618459f7558a"
+ integrity sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==
+ dependencies:
+ accepts "~1.3.4"
+ base64id "~2.0.0"
+ cors "~2.8.5"
+ debug "~4.3.2"
+ engine.io "~6.6.0"
+ socket.io-adapter "~2.5.2"
+ socket.io-parser "~4.2.4"
+
+statuses@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
+ integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
+
+supports-color@^5.5.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toidentifier@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
+ integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
+
+touch@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
+ integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
+
+type-is@~1.6.18:
+ version "1.6.18"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
+ integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.24"
+
+undefsafe@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
+ integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
+
+undici-types@~7.8.0:
+ version "7.8.0"
+ resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
+ integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
+
+vary@^1, vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
+
+ws@~8.17.1:
+ version "8.17.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
+ integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==