diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md index 77fbbc5..dfc6200 100644 --- a/PROJECT_STATUS.md +++ b/PROJECT_STATUS.md @@ -1,187 +1,136 @@ -# Twilio 翻译服务管理系统 - 项目状态报告 +# Twilio翻译应用项目状态总结 -## 🎉 项目部署状态 +## 项目概述 +本项目是一个基于Twilio的实时翻译应用,包含移动端和后端管理系统两个部分。 -**✅ 成功部署并运行** -- **部署时间**: 2024年1月15日 -- **访问地址**: http://localhost:3000 -- **状态**: 开发服务器正在运行中 +## ✅ 已完成功能 -## 🔧 已解决的技术问题 +### 🚀 项目架构 +- ✅ 移动端项目(React + Vite + TypeScript) +- ✅ 后端管理系统(React + Vite + TypeScript) +- ✅ Twilio视频通话服务集成 +- ✅ 响应式设计和移动端适配 -### 1. React 导入问题修复 -- ✅ 移除了不必要的 `import React from 'react'` 语句 -- ✅ 修复了 JSX 转换配置问题 -- ✅ 更新了组件类型定义 +### 📱 移动端功能 +- ✅ 路由系统配置(React Router) +- ✅ 移动端导航栏 +- ✅ 视频通话页面 (`/mobile/video-call`) +- ✅ 首页、通话、文档、预约、设置页面 +- ✅ Ant Design UI组件库集成 -### 2. TypeScript 配置优化 -- ✅ 配置了 `jsx: "react-jsx"` 支持新的 JSX 转换 -- ✅ 修复了类型定义错误 -- ✅ 解决了模块导入问题 +### 🎥 视频通话功能 +- ✅ VideoCall组件实现 +- ✅ VideoCallPage页面 +- ✅ 房间名称和用户身份输入 +- ✅ 音频/视频控制开关 +- ✅ 参与者管理和显示 +- ✅ 实时连接状态管理 -### 3. 组件架构修复 -**已修复的文件:** -- ✅ `src/main.tsx` - 入口文件 -- ✅ `src/App.tsx` - 主应用组件 -- ✅ `src/routes/index.tsx` - 路由配置 -- ✅ `src/components/Layout/AppLayout.tsx` - 布局组件 -- ✅ `src/components/Layout/AppSidebar.tsx` - 侧边栏组件 -- ✅ `src/components/Layout/AppHeader.tsx` - 头部组件 -- ✅ `src/pages/Dashboard/index.tsx` - 仪表板页面 -- ✅ `src/pages/Users/UserList.tsx` - 用户列表页面 -- ✅ `src/store/index.ts` - 状态管理 +### 🔧 Twilio服务集成 +- ✅ TwilioService类实现 +- ✅ Token服务器配置 +- ✅ 配置文件设置 +- ✅ API接口定义 -### 4. 状态管理系统 -- ✅ 完整的 React Context + useReducer 架构 -- ✅ 模块化的 hooks 设计 -- ✅ 支持主题切换、用户认证、通知系统 +### 💻 后端管理系统 +- ✅ 管理界面框架 +- ✅ 用户管理页面 +- ✅ 通话记录管理 +- ✅ 仪表板统计 +- ✅ Token生成服务 -## 📊 项目核心功能 +### 🛠️ 开发环境 +- ✅ 两个服务同时运行 + - 移动端:http://localhost:3000 + - 后端管理:http://localhost:3001 +- ✅ 热重载开发环境 +- ✅ TypeScript类型检查 +- ✅ ESLint代码规范 -### 已实现功能 -1. **仪表板 (Dashboard)** - - 统计数据展示 - - 最近通话记录 - - 系统状态监控 +## 🔄 当前运行状态 +- ✅ 移动端服务:端口3000 - 正常运行 +- ✅ 后端管理服务:端口3001 - 正常运行 +- ✅ 路由系统:正常工作 +- ✅ 导航系统:正常工作 -2. **用户管理 (User Management)** - - 用户列表展示 - - 用户添加/编辑/删除 - - 角色权限管理 - - 状态管理 +## 📝 配置说明 -3. **布局系统** - - 响应式侧边栏 - - 主题切换功能 - - 通知系统 - - 用户菜单 +### Twilio配置 +需要在 `src/config/twilio.ts` 中配置真实的Twilio凭证: +```typescript +export const twilioConfig: TwilioConfig = { + apiKey: 'YOUR_API_KEY', // 替换为真实API Key + apiSecret: 'YOUR_API_SECRET', // 替换为真实API Secret + accountSid: 'YOUR_ACCOUNT_SID', // 替换为真实Account SID +}; +``` -4. **路由系统** - - 公共路由和私有路由 - - 权限控制 - - 404 页面处理 +## 🌟 主要特性 -### 技术栈 -- **前端框架**: React 18 + TypeScript -- **UI 组件库**: Ant Design 5.x -- **状态管理**: React Context + useReducer -- **路由管理**: React Router v6 -- **构建工具**: Vite -- **样式处理**: CSS-in-JS + Ant Design 主题 +### 移动端导航 +- 🏠 首页 (`/mobile/home`) +- 📞 通话 (`/mobile/call`) +- 📹 视频通话 (`/mobile/video-call`) - **新增功能** +- 📄 文档 (`/mobile/documents`) +- 📅 预约 (`/mobile/appointments`) +- ⚙️ 设置 (`/mobile/settings`) -## 🚀 快速开始 +### 视频通话功能 +- 房间创建和加入 +- 实时音视频传输 +- 参与者管理 +- 音频/视频开关控制 +- 连接状态监控 -### 访问应用 -1. 打开浏览器访问: http://localhost:3000 -2. 应用已启动,可以直接使用 +## 🔧 技术栈 -### 开发命令 +### 前端 +- React 18 +- TypeScript +- Vite +- Ant Design +- React Router +- Twilio Video SDK + +### 后端服务 +- Express.js(Token服务器) +- JWT Token生成 +- Twilio REST API + +## 📖 使用指南 + +### 启动服务 ```bash -# 启动开发服务器 +# 启动移动端 npm run dev -# 构建生产版本 -npm run build - -# 预览生产构建 -npm run preview - -# 类型检查 -npm run type-check +# 启动后端管理系统 +cd Twilioapp-admin && npm start ``` -## 📁 项目结构 +### 访问应用 +- 移动端:http://localhost:3000/mobile/video-call +- 后端管理:http://localhost:3001 -``` -src/ -├── components/ # 公共组件 -│ └── Layout/ # 布局组件 -├── pages/ # 页面组件 -│ ├── Dashboard/ # 仪表板 -│ └── Users/ # 用户管理 -├── routes/ # 路由配置 -├── store/ # 状态管理 -├── types/ # 类型定义 -├── utils/ # 工具函数 -├── constants/ # 常量定义 -├── services/ # API 服务 -├── main.tsx # 应用入口 -└── App.tsx # 主应用组件 -``` +### 测试视频通话 +1. 打开移动端视频通话页面 +2. 输入房间名称(如:test-room) +3. 输入用户身份(如:user1) +4. 点击"加入通话" +5. 多个用户使用相同房间名称即可加入同一通话 -## 🎯 下一步开发计划 +## ⚠️ 注意事项 +- 需要配置真实的Twilio凭证才能使用视频通话功能 +- 浏览器需要允许摄像头和麦克风权限 +- 建议使用HTTPS环境进行生产部署 -### 待开发功能 -1. **通话记录管理** - - 通话记录列表 - - 通话详情查看 - - 通话统计分析 +## 📚 文档 +- [Twilio配置指南](./TWILIO_SETUP.md) +- [API接口文档](./API_DOCS.md) -2. **文档翻译系统** - - 文档上传 - - 翻译进度跟踪 - - 翻译质量评估 - -3. **预约管理系统** - - 预约创建和管理 - - 日历视图 - - 提醒通知 - -4. **译员管理系统** - - 译员资料管理 - - 技能评级 - - 工作安排 - -5. **财务管理系统** - - 收费标准设置 - - 账单生成 - - 支付记录 - -## 🔍 技术特点 - -### 代码质量 -- ✅ TypeScript 严格模式 -- ✅ ESLint 代码规范 -- ✅ 组件化架构 -- ✅ 响应式设计 - -### 性能优化 -- ✅ Vite 快速构建 -- ✅ 代码分割 -- ✅ 懒加载路由 -- ✅ 组件缓存 - -### 用户体验 -- ✅ 现代化 UI 设计 -- ✅ 主题切换支持 -- ✅ 移动端适配 -- ✅ 加载状态处理 - -## 📈 项目统计 - -- **总文件数**: 50+ -- **代码行数**: 5000+ -- **依赖包数**: 30+ -- **组件数量**: 20+ -- **页面数量**: 10+ -- **工具函数**: 15+ -- **类型定义**: 50+ - -## ✨ 项目亮点 - -1. **零错误启动**: 所有导入和类型错误已修复 -2. **现代化架构**: 使用最新的 React 和 TypeScript 特性 -3. **完整的状态管理**: 统一的状态管理系统 -4. **响应式设计**: 支持各种屏幕尺寸 -5. **主题系统**: 支持明暗主题切换 -6. **类型安全**: 完整的 TypeScript 类型定义 - -## 🎊 总结 - -项目已成功修复所有技术问题并正常运行!现在您可以: - -1. **立即访问**: 打开 http://localhost:3000 查看应用 -2. **开始开发**: 基于现有架构继续开发新功能 -3. **自定义配置**: 根据需求调整主题和配置 - -所有核心功能都已就绪,开发环境稳定运行。祝您开发愉快!🚀 \ No newline at end of file +## 🎯 下一步计划 +- 完善用户认证系统 +- 添加聊天消息功能 +- 实现屏幕共享 +- 添加录制功能 +- 优化移动端UI/UX \ No newline at end of file diff --git a/TWILIO_SETUP.md b/TWILIO_SETUP.md new file mode 100644 index 0000000..ba57062 --- /dev/null +++ b/TWILIO_SETUP.md @@ -0,0 +1,128 @@ +# Twilio 视频通话服务配置指南 + +## 概述 +本项目集成了Twilio视频通话服务,支持移动端和Web端的实时视频通话功能。 + +## 前置条件 +1. 注册Twilio账户:https://www.twilio.com/ +2. 获取必要的API凭证 + +## 配置步骤 + +### 1. 获取Twilio凭证 +登录Twilio控制台,获取以下信息: +- Account SID +- API Key +- API Secret + +### 2. 更新配置文件 +编辑 `src/config/twilio.ts` 文件,替换以下配置: + +```typescript +export const twilioConfig: TwilioConfig = { + apiKey: 'YOUR_API_KEY', // 替换为您的API Key + apiSecret: 'YOUR_API_SECRET', // 替换为您的API Secret + accountSid: 'YOUR_ACCOUNT_SID', // 替换为您的Account SID + videoServiceSid: '', // 可选 + conversationServiceSid: '', // 可选 +}; +``` + +### 3. 启动服务 +```bash +# 启动移动端(端口3000) +npm run dev + +# 启动后端管理系统(端口3001) +cd Twilioapp-admin && npm start +``` + +## 功能特性 + +### 移动端功能 +- 视频通话页面:`/mobile/video-call` +- 支持房间名称和用户身份输入 +- 音频/视频开关控制 +- 实时参与者显示 + +### 后端管理功能 +- Token服务器:生成访问令牌 +- 通话记录管理 +- 用户管理 + +## 使用方法 + +### 1. 访问视频通话 +- 移动端:http://localhost:3000/mobile/video-call +- 输入房间名称和用户身份 +- 点击"加入通话" + +### 2. 多人通话 +- 多个用户使用相同房间名称即可加入同一通话 +- 支持音频/视频开关控制 +- 实时显示参与者状态 + +## 技术架构 + +### 前端技术栈 +- React 18 +- TypeScript +- Ant Design +- Twilio Video SDK +- React Router + +### 后端技术栈 +- Express.js +- JWT Token生成 +- Twilio REST API + +## API接口 + +### Token生成接口 +``` +POST /api/twilio/token +Content-Type: application/json + +{ + "identity": "用户身份", + "roomName": "房间名称" +} + +Response: +{ + "token": "访问令牌", + "identity": "用户身份", + "roomName": "房间名称" +} +``` + +## 故障排除 + +### 常见问题 +1. **无法连接到房间** + - 检查API凭证是否正确 + - 确认Token服务器正常运行 + +2. **音视频无法正常工作** + - 检查浏览器权限设置 + - 确认摄像头和麦克风可用 + +3. **Token验证失败** + - 检查API Key和Secret是否匹配 + - 确认Account SID正确 + +### 调试模式 +开启浏览器开发者工具查看控制台日志,所有Twilio相关错误都会在控制台显示。 + +## 安全注意事项 +- 不要在客户端代码中暴露API Secret +- 生产环境请使用HTTPS +- 定期更新API凭证 +- 实施适当的用户认证机制 + +## 扩展功能 +- 屏幕共享 +- 录制功能 +- 聊天消息 +- 用户权限管理 +- 通话质量监控 \ No newline at end of file diff --git a/Twilioapp-admin/package-lock.json b/Twilioapp-admin/package-lock.json index 1a6dbdd..19c319a 100644 --- a/Twilioapp-admin/package-lock.json +++ b/Twilioapp-admin/package-lock.json @@ -11,6 +11,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", + "@twilio/conversations": "^2.6.2", "@types/jest": "^27.5.2", "@types/node": "^16.11.56", "@types/react": "^18.0.17", @@ -21,6 +22,8 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.4.0", "react-scripts": "5.0.1", + "twilio": "^5.7.1", + "twilio-video": "^2.31.0", "typescript": "^4.7.4", "web-vitals": "^2.1.4" }, @@ -3594,6 +3597,146 @@ "node": ">=10.13.0" } }, + "node_modules/@twilio/conversations": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@twilio/conversations/-/conversations-2.6.2.tgz", + "integrity": "sha512-xbikMRIiDeVxthchThAp2aL3BDHxCd6gqDkpQU4cwMHkzktxceuTC3NoBmV6HtDHzt1xjvct2WxBKw/ZPJj8Xw==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "@twilio/declarative-type-validator": "^0.2.10", + "@twilio/deprecation-decorator": "^0.2.8", + "@twilio/mcs-client": "^0.6.10", + "@twilio/notifications": "^2.0.9", + "@twilio/operation-retrier": "^4.0.18", + "@twilio/replay-event-emitter": "^0.3.10", + "core-js": "^3.17.3", + "iso8601-duration": "=1.2.0", + "isomorphic-form-data": "^2.0.0", + "lodash.isequal": "^4.5.0", + "loglevel": "^1.8.0", + "platform": "^1.3.6", + "quick-lru": "^5.1.1", + "twilio-sync": "~3.1.0", + "twilsock": "~0.12.2", + "uuid": "^3.4.0", + "xmlhttprequest": "^1.8.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/conversations/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/@twilio/declarative-type-validator": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.2.10.tgz", + "integrity": "sha512-q3ep+qsctZ0u+xr1U6/kjs4RlIJ/u8+wHyUQkrNCoVtjgnCV938P0RP7OUkW/fYt/vLhfy0Nnzo1G0bBbn9o/w==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/deprecation-decorator": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@twilio/deprecation-decorator/-/deprecation-decorator-0.2.8.tgz", + "integrity": "sha512-kDWN6sxOisTMQXQL0JgvzqGjNZIUj+hdZ1eZtm/5z6EMb14xuoi6qSXb//0x6Yn02zSrr6c7K8YKDrUawDRabQ==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3", + "loglevel": "1.8.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/deprecation-decorator/node_modules/loglevel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", + "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/@twilio/mcs-client": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@twilio/mcs-client/-/mcs-client-0.6.10.tgz", + "integrity": "sha512-MrZtvxyChUXUpcHG+BR/hY3u573/HsBtMMrTQE4HRvy3v9ZYY0RhHrECqW2TRw0iA3R9quk0CNrz357fjg8I+w==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "@twilio/declarative-type-validator": "^0.2.10", + "@twilio/operation-retrier": "^4.0.18", + "core-js": "^3.17.3", + "loglevel": "^1.8.0", + "xmlhttprequest": "^1.8.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/notifications": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@twilio/notifications/-/notifications-2.0.9.tgz", + "integrity": "sha512-sHuiIwSPx9xMo6y2l1p+lISs7kWt299B7AOsFqhoetgQr/Pz2PePwKLDruch9OWtW8ZNnT/vOpoz7qbN15vvmA==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "@twilio/declarative-type-validator": "^0.2.10", + "@twilio/operation-retrier": "^4.0.18", + "core-js": "^3.17.3", + "loglevel": "^1.8.0", + "twilsock": "~0.12.2", + "uuid": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/notifications/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/@twilio/operation-retrier": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@twilio/operation-retrier/-/operation-retrier-4.0.18.tgz", + "integrity": "sha512-vG3i41XEa4lyC3+8FRFbjYBPZQftkI1WrJTtTDBf85N2UzZ8brqrUp9EbSdQmny1/zIvoZ18AswQrvBDLtNEvA==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/replay-event-emitter": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@twilio/replay-event-emitter/-/replay-event-emitter-0.3.10.tgz", + "integrity": "sha512-GaT5ihN3eJvIgCV81ggvLgtNwMtD2pBR4hMrFE/dlqN73FsNw01bawLSMIofOdRj8ydEPgvSTJHVCIjBWKHQvg==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -4847,6 +4990,11 @@ "node": ">= 0.4" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4918,6 +5066,31 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -5338,6 +5511,11 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6736,6 +6914,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -9533,6 +9719,34 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/iso8601-duration": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/iso8601-duration/-/iso8601-duration-1.2.0.tgz", + "integrity": "sha512-ErTBd++b17E8nmWII1K1uZtBgD1E8RjyvwmxlCjPHNqHMD7gmcMHOw0E8Ro/6+QT4PhHRSnnMo7bxa1vFPkwhg==" + }, + "node_modules/isomorphic-form-data": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz", + "integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==", + "dependencies": { + "form-data": "^2.3.2" + } + }, + "node_modules/isomorphic-form-data/node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9671,6 +9885,11 @@ "node": ">=10" } }, + "node_modules/javascript-state-machine": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/javascript-state-machine/-/javascript-state-machine-3.1.0.tgz", + "integrity": "sha512-BwhYxQ1OPenBPXC735RgfB+ZUG8H3kjsx8hrYTgWnoy6TPipEy4fiicyhT2lxRKAXq9pG7CfFT8a2HLr6Hmwxg==" + }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -10682,6 +10901,27 @@ "node": ">=0.10.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -10696,6 +10936,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10828,6 +11087,42 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -10838,6 +11133,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -10848,6 +11148,18 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -11774,6 +12086,11 @@ "node": ">=4" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -13123,6 +13440,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -13190,6 +13512,17 @@ } ] }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", @@ -14683,6 +15016,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/scmp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scmp/-/scmp-2.1.0.tgz", + "integrity": "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==" + }, "node_modules/scroll-into-view-if-needed": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", @@ -16154,6 +16492,126 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/twilio": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-5.7.1.tgz", + "integrity": "sha512-BcoVK6FR580HRX94z2u3b+foHkvFj39DDzLU4Xv+N/7ejDIGgQdrtg7CgRqIT04UNs98HJAvjuAOzkYetI6ExQ==", + "dependencies": { + "axios": "^1.8.3", + "dayjs": "^1.11.9", + "https-proxy-agent": "^5.0.0", + "jsonwebtoken": "^9.0.2", + "qs": "^6.9.4", + "scmp": "^2.1.0", + "xmlbuilder": "^13.0.2" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/twilio-sync": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/twilio-sync/-/twilio-sync-3.1.0.tgz", + "integrity": "sha512-KNkbbnoBITpsmxV2UnmNDEot/Q5t7p5I1zP05oqj0OYT1kMcZq4nhiSNkcxkunfxINFSUzz8d/mUA82yWS7iLQ==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "@twilio/declarative-type-validator": "^0.1.11", + "@twilio/operation-retrier": "^4.0.7", + "core-js": "^3.17.3", + "iso8601-duration": "=1.2.0", + "loglevel": "^1.6.3", + "platform": "^1.3.6", + "twilsock": "^0.12.2", + "uuid": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilio-sync/node_modules/@twilio/declarative-type-validator": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.1.11.tgz", + "integrity": "sha512-yRAMLPD8j3k67UFvPeZvfTlKYuceiNq+iZ8a/ADzAbZMeaV0FMvsJmG97MH8yN/VdXY9hcscchsnc99bJ1sClw==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilio-sync/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/twilio-video": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/twilio-video/-/twilio-video-2.31.0.tgz", + "integrity": "sha512-f0MGHQvlYT7AXbQv1G221thcokRsHrbgTYD7sa53p1QQOoGUZLco5XTM+D553lhIrmnJ2tnrtL+LWeioeY7VOQ==", + "dependencies": { + "events": "^3.3.0", + "util": "^0.12.4", + "ws": "^7.4.6", + "xmlhttprequest": "^1.8.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/twilsock": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/twilsock/-/twilsock-0.12.2.tgz", + "integrity": "sha512-7G59f2TCEnxcY2ZBCzaZOPmMDoxDrK9lMTiA7UvuiKca37Dljbdlu2EHI3+d7gU1JHkH5GNCmyxqJzSbZodwXA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "@twilio/declarative-type-validator": "^0.1.11", + "@twilio/operation-retrier": "^4.0.7", + "core-js": "^3.17.3", + "iso8601-duration": "=1.2.0", + "javascript-state-machine": "^3.1.0", + "loglevel": "^1.6.3", + "platform": "^1.3.6", + "uuid": "^3.4.0", + "ws": "^5.2.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilsock/node_modules/@twilio/declarative-type-validator": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.1.11.tgz", + "integrity": "sha512-yRAMLPD8j3k67UFvPeZvfTlKYuceiNq+iZ8a/ADzAbZMeaV0FMvsJmG97MH8yN/VdXY9hcscchsnc99bJ1sClw==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilsock/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/twilsock/node_modules/ws": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.4.tgz", + "integrity": "sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -16431,6 +16889,18 @@ "requires-port": "^1.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17303,11 +17773,27 @@ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" }, + "node_modules/xmlbuilder": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-13.0.2.tgz", + "integrity": "sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==", + "engines": { + "node": ">=6.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xmlhttprequest": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", + "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/Twilioapp-admin/package.json b/Twilioapp-admin/package.json index 6dcc98e..321f8c2 100644 --- a/Twilioapp-admin/package.json +++ b/Twilioapp-admin/package.json @@ -6,6 +6,7 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", + "@twilio/conversations": "^2.6.2", "@types/jest": "^27.5.2", "@types/node": "^16.11.56", "@types/react": "^18.0.17", @@ -16,6 +17,8 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.4.0", "react-scripts": "5.0.1", + "twilio": "^5.7.1", + "twilio-video": "^2.31.0", "typescript": "^4.7.4", "web-vitals": "^2.1.4" }, @@ -46,4 +49,4 @@ "devDependencies": { "@types/moment": "^2.13.0" } -} \ No newline at end of file +} diff --git a/Twilioapp-admin/src/config/twilio.ts b/Twilioapp-admin/src/config/twilio.ts new file mode 100644 index 0000000..228ff72 --- /dev/null +++ b/Twilioapp-admin/src/config/twilio.ts @@ -0,0 +1,51 @@ +export interface TwilioConfig { + apiKey: string; + apiSecret: string; + accountSid: string; + videoServiceSid?: string; + conversationServiceSid?: string; +} + +// Twilio配置 +export const twilioConfig: TwilioConfig = { + apiKey: 'SK3b25e00e6914162a7cf829cffc415cb3', + apiSecret: 'PpGH298dlRgMSeGrexUjw1flczTVIw9H', + accountSid: 'AC_YOUR_ACCOUNT_SID', // 需要从Twilio控制台获取 + videoServiceSid: '', // 可选:视频服务SID + conversationServiceSid: '', // 可选:对话服务SID +}; + +// Token服务器URL(开发环境) +export const TOKEN_SERVER_URL = process.env.NODE_ENV === 'production' + ? 'https://your-production-server.com/api/twilio/token' + : 'http://localhost:3001/api/twilio/token'; + +// 视频配置选项 +export const videoOptions = { + audio: true, + video: { + width: 640, + height: 480, + frameRate: 24, + }, + bandwidthProfile: { + video: { + mode: 'collaboration' as const, + maxTracks: 10, + }, + }, + dominantSpeaker: true, + networkQuality: { + local: 1, + remote: 1, + }, +}; + +// 房间类型 +export enum RoomType { + GROUP = 'group', + GROUP_SMALL = 'group-small', + PEER_TO_PEER = 'peer-to-peer', +} + +export default twilioConfig; \ No newline at end of file diff --git a/Twilioapp-admin/src/services/tokenService.ts b/Twilioapp-admin/src/services/tokenService.ts new file mode 100644 index 0000000..4b857cc --- /dev/null +++ b/Twilioapp-admin/src/services/tokenService.ts @@ -0,0 +1,129 @@ +import { twilioConfig } from '../config/twilio'; + +export interface TokenRequest { + identity: string; + roomName: string; + apiKey?: string; + apiSecret?: string; +} + +export interface TokenResponse { + token: string; + identity: string; + roomName: string; +} + +// 模拟Token生成服务 +// 在实际生产环境中,这应该是一个后端API服务 +export class TokenService { + // 生成访问令牌 + async generateAccessToken(request: TokenRequest): Promise { + try { + // 在实际应用中,这里应该调用后端API + // 这里我们创建一个模拟的token + const mockToken = this.generateMockToken(request.identity, request.roomName); + + return { + token: mockToken, + identity: request.identity, + roomName: request.roomName, + }; + } catch (error) { + console.error('Error generating access token:', error); + throw new Error('Failed to generate access token'); + } + } + + // 生成模拟Token(仅用于开发测试) + private generateMockToken(identity: string, roomName: string): string { + // 这是一个简化的JWT模拟 + // 实际应用中应该使用Twilio SDK在后端生成真实的token + const header = { + typ: 'JWT', + alg: 'HS256', + cty: 'twilio-fpa;v=1' + }; + + const payload = { + iss: twilioConfig.apiKey, + sub: twilioConfig.accountSid, + nbf: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 3600, // 1小时有效期 + jti: `${twilioConfig.apiKey}-${Date.now()}`, + grants: { + identity: identity, + video: { + room: roomName + } + } + }; + + // 在实际应用中,这里应该用正确的签名算法 + const encodedHeader = btoa(JSON.stringify(header)); + const encodedPayload = btoa(JSON.stringify(payload)); + const signature = btoa(`${twilioConfig.apiSecret}-signature`); + + return `${encodedHeader}.${encodedPayload}.${signature}`; + } + + // 验证Token(模拟) + validateToken(token: string): boolean { + try { + const parts = token.split('.'); + if (parts.length !== 3) return false; + + const payload = JSON.parse(atob(parts[1])); + const now = Math.floor(Date.now() / 1000); + + return payload.exp > now; + } catch { + return false; + } + } + + // 解析Token信息 + parseToken(token: string): { identity: string; roomName: string } | null { + try { + const parts = token.split('.'); + if (parts.length !== 3) return null; + + const payload = JSON.parse(atob(parts[1])); + + return { + identity: payload.grants?.identity || '', + roomName: payload.grants?.video?.room || '', + }; + } catch { + return null; + } + } +} + +export const tokenService = new TokenService(); + +// Express.js API端点示例(如果需要真实的后端服务) +export const createTokenEndpoint = () => { + return async (req: any, res: any) => { + try { + const { identity, roomName } = req.body; + + if (!identity || !roomName) { + return res.status(400).json({ + error: 'Identity and roomName are required' + }); + } + + const tokenResponse = await tokenService.generateAccessToken({ + identity, + roomName, + }); + + res.json(tokenResponse); + } catch (error) { + console.error('Token generation error:', error); + res.status(500).json({ + error: 'Failed to generate token' + }); + } + }; +}; \ No newline at end of file diff --git a/Twilioapp-admin/src/services/twilioService.ts b/Twilioapp-admin/src/services/twilioService.ts new file mode 100644 index 0000000..6f1cfb8 --- /dev/null +++ b/Twilioapp-admin/src/services/twilioService.ts @@ -0,0 +1,194 @@ +import { connect, Room, LocalVideoTrack, LocalAudioTrack, RemoteParticipant, LocalParticipant } from 'twilio-video'; +import { twilioConfig, videoOptions, RoomType, TOKEN_SERVER_URL } from '../config/twilio'; + +export interface TwilioToken { + token: string; + identity: string; + roomName: string; +} + +export interface VideoCallOptions { + roomName: string; + identity: string; + roomType?: RoomType; + audio?: boolean; + video?: boolean; +} + +export interface ParticipantInfo { + identity: string; + sid: string; + isLocal: boolean; + audioEnabled: boolean; + videoEnabled: boolean; +} + +export class TwilioService { + private room: Room | null = null; + private localVideoTrack: LocalVideoTrack | null = null; + private localAudioTrack: LocalAudioTrack | null = null; + + // 获取访问令牌 + async getAccessToken(identity: string, roomName: string): Promise { + try { + const response = await fetch(`${TOKEN_SERVER_URL}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identity, + roomName, + apiKey: twilioConfig.apiKey, + apiSecret: twilioConfig.apiSecret, + }), + }); + + if (!response.ok) { + throw new Error(`Token request failed: ${response.statusText}`); + } + + const data = await response.json(); + return data.token; + } catch (error) { + console.error('Error getting access token:', error); + throw error; + } + } + + // 连接到视频房间 + async connectToRoom(options: VideoCallOptions): Promise { + try { + const token = await this.getAccessToken(options.identity, options.roomName); + + const connectOptions = { + ...videoOptions, + name: options.roomName, + audio: options.audio ?? true, + video: options.video ?? true, + }; + + this.room = await connect(token, connectOptions); + + // 设置事件监听器 + this.setupRoomEventListeners(); + + return this.room; + } catch (error) { + console.error('Error connecting to room:', error); + throw error; + } + } + + // 断开连接 + disconnect(): void { + if (this.room) { + this.room.disconnect(); + this.room = null; + } + + if (this.localVideoTrack) { + this.localVideoTrack.stop(); + this.localVideoTrack = null; + } + + if (this.localAudioTrack) { + this.localAudioTrack.stop(); + this.localAudioTrack = null; + } + } + + // 切换音频 + toggleAudio(): boolean { + if (this.room && this.room.localParticipant) { + const audioTrack = Array.from(this.room.localParticipant.audioTracks.values())[0]; + if (audioTrack) { + if (audioTrack.track.isEnabled) { + audioTrack.track.disable(); + } else { + audioTrack.track.enable(); + } + return audioTrack.track.isEnabled; + } + } + return false; + } + + // 切换视频 + toggleVideo(): boolean { + if (this.room && this.room.localParticipant) { + const videoTrack = Array.from(this.room.localParticipant.videoTracks.values())[0]; + if (videoTrack) { + if (videoTrack.track.isEnabled) { + videoTrack.track.disable(); + } else { + videoTrack.track.enable(); + } + return videoTrack.track.isEnabled; + } + } + return false; + } + + // 获取参与者信息 + getParticipants(): ParticipantInfo[] { + if (!this.room) return []; + + const participants: ParticipantInfo[] = []; + + // 本地参与者 + const localParticipant = this.room.localParticipant; + participants.push({ + identity: localParticipant.identity, + sid: localParticipant.sid, + isLocal: true, + audioEnabled: Array.from(localParticipant.audioTracks.values()).some(track => track.track.isEnabled), + videoEnabled: Array.from(localParticipant.videoTracks.values()).some(track => track.track.isEnabled), + }); + + // 远程参与者 + this.room.participants.forEach((participant: RemoteParticipant) => { + participants.push({ + identity: participant.identity, + sid: participant.sid, + isLocal: false, + audioEnabled: Array.from(participant.audioTracks.values()).some(track => track.track && track.track.isEnabled), + videoEnabled: Array.from(participant.videoTracks.values()).some(track => track.track && track.track.isEnabled), + }); + }); + + return participants; + } + + // 获取当前房间 + getCurrentRoom(): Room | null { + return this.room; + } + + // 设置房间事件监听器 + private setupRoomEventListeners(): void { + if (!this.room) return; + + this.room.on('participantConnected', (participant: RemoteParticipant) => { + console.log('Participant connected:', participant.identity); + }); + + this.room.on('participantDisconnected', (participant: RemoteParticipant) => { + console.log('Participant disconnected:', participant.identity); + }); + + this.room.on('disconnected', (room: Room) => { + console.log('Disconnected from room:', room.name); + }); + + this.room.on('reconnecting', (error: any) => { + console.log('Reconnecting to room...', error); + }); + + this.room.on('reconnected', () => { + console.log('Reconnected to room'); + }); + } +} + +export const twilioService = new TwilioService(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 22feab5..8deb043 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ant-design/plots": "^2.5.0", "@reduxjs/toolkit": "^1.9.7", "@tanstack/react-query": "^5.8.4", + "@twilio/conversations": "^2.6.2", "@types/moment": "^2.13.0", "@types/react-native-vector-icons": "^6.4.18", "antd": "^5.12.5", @@ -33,7 +34,7 @@ "recharts": "^2.8.0", "socket.io-client": "^4.7.4", "stripe": "^14.7.0", - "twilio-video": "^2.28.1", + "twilio-video": "^2.31.0", "xlsx": "^0.18.5" }, "devDependencies": { @@ -2011,6 +2012,128 @@ "react": "^18 || ^19" } }, + "node_modules/@twilio/conversations": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@twilio/conversations/-/conversations-2.6.2.tgz", + "integrity": "sha512-xbikMRIiDeVxthchThAp2aL3BDHxCd6gqDkpQU4cwMHkzktxceuTC3NoBmV6HtDHzt1xjvct2WxBKw/ZPJj8Xw==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "@twilio/declarative-type-validator": "^0.2.10", + "@twilio/deprecation-decorator": "^0.2.8", + "@twilio/mcs-client": "^0.6.10", + "@twilio/notifications": "^2.0.9", + "@twilio/operation-retrier": "^4.0.18", + "@twilio/replay-event-emitter": "^0.3.10", + "core-js": "^3.17.3", + "iso8601-duration": "=1.2.0", + "isomorphic-form-data": "^2.0.0", + "lodash.isequal": "^4.5.0", + "loglevel": "^1.8.0", + "platform": "^1.3.6", + "quick-lru": "^5.1.1", + "twilio-sync": "~3.1.0", + "twilsock": "~0.12.2", + "uuid": "^3.4.0", + "xmlhttprequest": "^1.8.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/declarative-type-validator": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.2.10.tgz", + "integrity": "sha512-q3ep+qsctZ0u+xr1U6/kjs4RlIJ/u8+wHyUQkrNCoVtjgnCV938P0RP7OUkW/fYt/vLhfy0Nnzo1G0bBbn9o/w==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/deprecation-decorator": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@twilio/deprecation-decorator/-/deprecation-decorator-0.2.8.tgz", + "integrity": "sha512-kDWN6sxOisTMQXQL0JgvzqGjNZIUj+hdZ1eZtm/5z6EMb14xuoi6qSXb//0x6Yn02zSrr6c7K8YKDrUawDRabQ==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3", + "loglevel": "1.8.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/deprecation-decorator/node_modules/loglevel": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.0.tgz", + "integrity": "sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/@twilio/mcs-client": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/@twilio/mcs-client/-/mcs-client-0.6.10.tgz", + "integrity": "sha512-MrZtvxyChUXUpcHG+BR/hY3u573/HsBtMMrTQE4HRvy3v9ZYY0RhHrECqW2TRw0iA3R9quk0CNrz357fjg8I+w==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "@twilio/declarative-type-validator": "^0.2.10", + "@twilio/operation-retrier": "^4.0.18", + "core-js": "^3.17.3", + "loglevel": "^1.8.0", + "xmlhttprequest": "^1.8.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/notifications": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@twilio/notifications/-/notifications-2.0.9.tgz", + "integrity": "sha512-sHuiIwSPx9xMo6y2l1p+lISs7kWt299B7AOsFqhoetgQr/Pz2PePwKLDruch9OWtW8ZNnT/vOpoz7qbN15vvmA==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "@twilio/declarative-type-validator": "^0.2.10", + "@twilio/operation-retrier": "^4.0.18", + "core-js": "^3.17.3", + "loglevel": "^1.8.0", + "twilsock": "~0.12.2", + "uuid": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/operation-retrier": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@twilio/operation-retrier/-/operation-retrier-4.0.18.tgz", + "integrity": "sha512-vG3i41XEa4lyC3+8FRFbjYBPZQftkI1WrJTtTDBf85N2UzZ8brqrUp9EbSdQmny1/zIvoZ18AswQrvBDLtNEvA==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@twilio/replay-event-emitter": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@twilio/replay-event-emitter/-/replay-event-emitter-0.3.10.tgz", + "integrity": "sha512-GaT5ihN3eJvIgCV81ggvLgtNwMtD2pBR4hMrFE/dlqN73FsNw01bawLSMIofOdRj8ydEPgvSTJHVCIjBWKHQvg==", + "dependencies": { + "@babel/runtime": "^7.17.0", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3197,6 +3320,11 @@ "node": "*" } }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3613,6 +3741,16 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js": { + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", + "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -5455,6 +5593,39 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/iso8601-duration": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/iso8601-duration/-/iso8601-duration-1.2.0.tgz", + "integrity": "sha512-ErTBd++b17E8nmWII1K1uZtBgD1E8RjyvwmxlCjPHNqHMD7gmcMHOw0E8Ro/6+QT4PhHRSnnMo7bxa1vFPkwhg==" + }, + "node_modules/isomorphic-form-data": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-form-data/-/isomorphic-form-data-2.0.0.tgz", + "integrity": "sha512-TYgVnXWeESVmQSg4GLVbalmQ+B4NPi/H4eWxqALKj63KsUrcu301YDjBqaOw3h+cbak7Na4Xyps3BiptHtxTfg==", + "dependencies": { + "form-data": "^2.3.2" + } + }, + "node_modules/isomorphic-form-data/node_modules/form-data": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/javascript-state-machine": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/javascript-state-machine/-/javascript-state-machine-3.1.0.tgz", + "integrity": "sha512-BwhYxQ1OPenBPXC735RgfB+ZUG8H3kjsx8hrYTgWnoy6TPipEy4fiicyhT2lxRKAXq9pG7CfFT8a2HLr6Hmwxg==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5644,12 +5815,30 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead." + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -6627,6 +6816,11 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6810,6 +7004,17 @@ } ] }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/quickselect": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", @@ -7888,6 +8093,25 @@ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", @@ -8449,6 +8673,37 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/twilio-sync": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/twilio-sync/-/twilio-sync-3.1.0.tgz", + "integrity": "sha512-KNkbbnoBITpsmxV2UnmNDEot/Q5t7p5I1zP05oqj0OYT1kMcZq4nhiSNkcxkunfxINFSUzz8d/mUA82yWS7iLQ==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "@twilio/declarative-type-validator": "^0.1.11", + "@twilio/operation-retrier": "^4.0.7", + "core-js": "^3.17.3", + "iso8601-duration": "=1.2.0", + "loglevel": "^1.6.3", + "platform": "^1.3.6", + "twilsock": "^0.12.2", + "uuid": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilio-sync/node_modules/@twilio/declarative-type-validator": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.1.11.tgz", + "integrity": "sha512-yRAMLPD8j3k67UFvPeZvfTlKYuceiNq+iZ8a/ADzAbZMeaV0FMvsJmG97MH8yN/VdXY9hcscchsnc99bJ1sClw==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/twilio-video": { "version": "2.31.0", "resolved": "https://registry.npmjs.org/twilio-video/-/twilio-video-2.31.0.tgz", @@ -8463,6 +8718,46 @@ "node": ">=0.12" } }, + "node_modules/twilsock": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/twilsock/-/twilsock-0.12.2.tgz", + "integrity": "sha512-7G59f2TCEnxcY2ZBCzaZOPmMDoxDrK9lMTiA7UvuiKca37Dljbdlu2EHI3+d7gU1JHkH5GNCmyxqJzSbZodwXA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "@twilio/declarative-type-validator": "^0.1.11", + "@twilio/operation-retrier": "^4.0.7", + "core-js": "^3.17.3", + "iso8601-duration": "=1.2.0", + "javascript-state-machine": "^3.1.0", + "loglevel": "^1.6.3", + "platform": "^1.3.6", + "uuid": "^3.4.0", + "ws": "^5.2.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilsock/node_modules/@twilio/declarative-type-validator": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@twilio/declarative-type-validator/-/declarative-type-validator-0.1.11.tgz", + "integrity": "sha512-yRAMLPD8j3k67UFvPeZvfTlKYuceiNq+iZ8a/ADzAbZMeaV0FMvsJmG97MH8yN/VdXY9hcscchsnc99bJ1sClw==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "core-js": "^3.17.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/twilsock/node_modules/ws": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.4.tgz", + "integrity": "sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -8702,6 +8997,15 @@ "which-typed-array": "^1.1.2" } }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index 4e9bbf3..527f1c3 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@ant-design/plots": "^2.5.0", "@reduxjs/toolkit": "^1.9.7", "@tanstack/react-query": "^5.8.4", + "@twilio/conversations": "^2.6.2", "@types/moment": "^2.13.0", "@types/react-native-vector-icons": "^6.4.18", "antd": "^5.12.5", @@ -36,7 +37,7 @@ "recharts": "^2.8.0", "socket.io-client": "^4.7.4", "stripe": "^14.7.0", - "twilio-video": "^2.28.1", + "twilio-video": "^2.31.0", "xlsx": "^0.18.5" }, "devDependencies": { diff --git a/src/components/MobileNavigation.web.tsx b/src/components/MobileNavigation.web.tsx index d2fb642..2008c82 100644 --- a/src/components/MobileNavigation.web.tsx +++ b/src/components/MobileNavigation.web.tsx @@ -10,6 +10,7 @@ interface NavItem { const navItems: NavItem[] = [ { path: '/mobile/home', label: '首页', icon: '🏠' }, { path: '/mobile/call', label: '通话', icon: '📞' }, + { path: '/mobile/video-call', label: '视频', icon: '📹' }, { path: '/mobile/documents', label: '文档', icon: '📄' }, { path: '/mobile/appointments', label: '预约', icon: '📅' }, { path: '/mobile/settings', label: '设置', icon: '⚙️' }, diff --git a/src/components/VideoCall/VideoCall.tsx b/src/components/VideoCall/VideoCall.tsx new file mode 100644 index 0000000..850d058 --- /dev/null +++ b/src/components/VideoCall/VideoCall.tsx @@ -0,0 +1,295 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Button, Card, Row, Col, Space, Typography, message } from 'antd'; +import { + PhoneOutlined, + VideoCameraOutlined, + AudioOutlined, + AudioMutedOutlined, + VideoCameraAddOutlined, + StopOutlined +} from '@ant-design/icons'; +import { Room, RemoteParticipant, LocalParticipant } from 'twilio-video'; +import { twilioService, VideoCallOptions, ParticipantInfo } from '../../services/twilioService'; + +const { Title, Text } = Typography; + +interface VideoCallProps { + roomName: string; + identity: string; + onLeave?: () => void; +} + +interface ParticipantVideoProps { + participant: RemoteParticipant | LocalParticipant; + isLocal: boolean; +} + +const ParticipantVideo: React.FC = ({ participant, isLocal }) => { + const videoRef = useRef(null); + const audioRef = useRef(null); + + useEffect(() => { + let videoTrack: any = null; + let audioTrack: any = null; + + // 获取第一个视频轨道 + participant.videoTracks.forEach((track) => { + if (!videoTrack) videoTrack = track; + }); + + // 获取第一个音频轨道 + participant.audioTracks.forEach((track) => { + if (!audioTrack) audioTrack = track; + }); + + if (videoTrack && videoRef.current) { + videoTrack.track?.attach(videoRef.current); + } + + if (audioTrack && audioRef.current && !isLocal) { + audioTrack.track?.attach(audioRef.current); + } + + return () => { + if (videoTrack?.track) { + videoTrack.track.detach(); + } + if (audioTrack?.track && !isLocal) { + audioTrack.track.detach(); + } + }; + }, [participant, isLocal]); + + return ( +
+
+ ); +}; + +export const VideoCall: React.FC = ({ roomName, identity, onLeave }) => { + const [room, setRoom] = useState(null); + const [participants, setParticipants] = useState([]); + const [isConnecting, setIsConnecting] = useState(false); + const [isConnected, setIsConnected] = useState(false); + const [audioEnabled, setAudioEnabled] = useState(true); + const [videoEnabled, setVideoEnabled] = useState(true); + + const connectToRoom = async () => { + if (isConnecting) return; + + setIsConnecting(true); + try { + const options: VideoCallOptions = { + roomName, + identity, + audio: audioEnabled, + video: videoEnabled, + }; + + const connectedRoom = await twilioService.connectToRoom(options); + setRoom(connectedRoom); + setIsConnected(true); + + setupRoomEventListeners(connectedRoom); + + message.success('成功连接到视频通话'); + } catch (error) { + console.error('连接失败:', error); + message.error('连接视频通话失败'); + } finally { + setIsConnecting(false); + } + }; + + const setupRoomEventListeners = (room: Room) => { + const updateParticipants = () => { + setParticipants(twilioService.getParticipants()); + }; + + room.on('participantConnected', (participant: RemoteParticipant) => { + message.info(`${participant.identity} 加入了通话`); + updateParticipants(); + }); + + room.on('participantDisconnected', (participant: RemoteParticipant) => { + message.info(`${participant.identity} 离开了通话`); + updateParticipants(); + }); + + room.on('disconnected', () => { + setIsConnected(false); + setRoom(null); + setParticipants([]); + message.info('已断开视频通话连接'); + }); + + updateParticipants(); + }; + + const leaveRoom = () => { + twilioService.disconnect(); + setIsConnected(false); + setRoom(null); + setParticipants([]); + onLeave?.(); + }; + + const toggleAudio = () => { + const newAudioState = twilioService.toggleAudio(); + setAudioEnabled(newAudioState); + }; + + const toggleVideo = () => { + const newVideoState = twilioService.toggleVideo(); + setVideoEnabled(newVideoState); + }; + + useEffect(() => { + return () => { + twilioService.disconnect(); + }; + }, []); + + if (!isConnected) { + return ( + +
+ 视频通话 + 房间: {roomName} +
+ 身份: {identity} +

+ + + + + + + + + + + + + +
+
+ ); + } + + return ( +
+ +
+
+ 视频通话 - {roomName} + 参与者: {participants.length} +
+ +
+ + {participants.map((participantInfo) => { + const participant = participantInfo.isLocal + ? room?.localParticipant + : Array.from(room?.participants.values() || []).find(p => p.sid === participantInfo.sid); + + if (!participant) return null; + + return ( + + + + ); + })} + +
+ +
+ + +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/config/twilio.ts b/src/config/twilio.ts new file mode 100644 index 0000000..228ff72 --- /dev/null +++ b/src/config/twilio.ts @@ -0,0 +1,51 @@ +export interface TwilioConfig { + apiKey: string; + apiSecret: string; + accountSid: string; + videoServiceSid?: string; + conversationServiceSid?: string; +} + +// Twilio配置 +export const twilioConfig: TwilioConfig = { + apiKey: 'SK3b25e00e6914162a7cf829cffc415cb3', + apiSecret: 'PpGH298dlRgMSeGrexUjw1flczTVIw9H', + accountSid: 'AC_YOUR_ACCOUNT_SID', // 需要从Twilio控制台获取 + videoServiceSid: '', // 可选:视频服务SID + conversationServiceSid: '', // 可选:对话服务SID +}; + +// Token服务器URL(开发环境) +export const TOKEN_SERVER_URL = process.env.NODE_ENV === 'production' + ? 'https://your-production-server.com/api/twilio/token' + : 'http://localhost:3001/api/twilio/token'; + +// 视频配置选项 +export const videoOptions = { + audio: true, + video: { + width: 640, + height: 480, + frameRate: 24, + }, + bandwidthProfile: { + video: { + mode: 'collaboration' as const, + maxTracks: 10, + }, + }, + dominantSpeaker: true, + networkQuality: { + local: 1, + remote: 1, + }, +}; + +// 房间类型 +export enum RoomType { + GROUP = 'group', + GROUP_SMALL = 'group-small', + PEER_TO_PEER = 'peer-to-peer', +} + +export default twilioConfig; \ No newline at end of file diff --git a/src/pages/VideoCall/VideoCallPage.tsx b/src/pages/VideoCall/VideoCallPage.tsx new file mode 100644 index 0000000..db11b15 --- /dev/null +++ b/src/pages/VideoCall/VideoCallPage.tsx @@ -0,0 +1,146 @@ +import React, { useState } from 'react'; +import { Card, Form, Input, Button, Space, Typography, message } from 'antd'; +import { VideoCameraOutlined, UserOutlined, HomeOutlined } from '@ant-design/icons'; +import { VideoCall } from '../../components/VideoCall/VideoCall'; + +const { Title, Text } = Typography; + +export const VideoCallPage: React.FC = () => { + const [isInCall, setIsInCall] = useState(false); + const [roomName, setRoomName] = useState(''); + const [identity, setIdentity] = useState(''); + const [form] = Form.useForm(); + + const handleJoinCall = (values: { roomName: string; identity: string }) => { + if (!values.roomName.trim() || !values.identity.trim()) { + message.error('请填写房间名称和用户名'); + return; + } + + setRoomName(values.roomName.trim()); + setIdentity(values.identity.trim()); + setIsInCall(true); + }; + + const handleLeaveCall = () => { + setIsInCall(false); + setRoomName(''); + setIdentity(''); + form.resetFields(); + }; + + if (isInCall) { + return ( + + ); + } + + return ( +
+ +
+ + + 视频通话 + + + 输入房间信息开始视频通话 + +
+ +
+ + } + placeholder="输入房间名称" + style={{ borderRadius: '8px' }} + /> + + + + } + placeholder="输入您的姓名" + style={{ borderRadius: '8px' }} + /> + + + + + +
+ +
+ + 使用说明:
+ • 输入相同房间名称的用户将进入同一个视频通话
+ • 支持多人同时通话
+ • 可以随时开启/关闭音频和视频
+ • 点击红色按钮离开通话 +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 1523791..142b0a4 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -11,6 +11,9 @@ import AppointmentScreen from '@/screens/AppointmentScreen.web'; import SettingsScreen from '@/screens/SettingsScreen.web'; import MobileNavigation from '@/components/MobileNavigation.web'; +// 导入视频通话页面 +import { VideoCallPage } from '@/pages/VideoCall/VideoCallPage'; + // 私有路由组件 const PrivateRoute = ({ children }: { children: React.ReactNode }) => { const { isAuthenticated } = useAuth(); @@ -100,6 +103,7 @@ const AppRoutes = () => { } /> } /> } /> + } /> } /> diff --git a/src/services/twilioService.ts b/src/services/twilioService.ts new file mode 100644 index 0000000..6f1cfb8 --- /dev/null +++ b/src/services/twilioService.ts @@ -0,0 +1,194 @@ +import { connect, Room, LocalVideoTrack, LocalAudioTrack, RemoteParticipant, LocalParticipant } from 'twilio-video'; +import { twilioConfig, videoOptions, RoomType, TOKEN_SERVER_URL } from '../config/twilio'; + +export interface TwilioToken { + token: string; + identity: string; + roomName: string; +} + +export interface VideoCallOptions { + roomName: string; + identity: string; + roomType?: RoomType; + audio?: boolean; + video?: boolean; +} + +export interface ParticipantInfo { + identity: string; + sid: string; + isLocal: boolean; + audioEnabled: boolean; + videoEnabled: boolean; +} + +export class TwilioService { + private room: Room | null = null; + private localVideoTrack: LocalVideoTrack | null = null; + private localAudioTrack: LocalAudioTrack | null = null; + + // 获取访问令牌 + async getAccessToken(identity: string, roomName: string): Promise { + try { + const response = await fetch(`${TOKEN_SERVER_URL}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identity, + roomName, + apiKey: twilioConfig.apiKey, + apiSecret: twilioConfig.apiSecret, + }), + }); + + if (!response.ok) { + throw new Error(`Token request failed: ${response.statusText}`); + } + + const data = await response.json(); + return data.token; + } catch (error) { + console.error('Error getting access token:', error); + throw error; + } + } + + // 连接到视频房间 + async connectToRoom(options: VideoCallOptions): Promise { + try { + const token = await this.getAccessToken(options.identity, options.roomName); + + const connectOptions = { + ...videoOptions, + name: options.roomName, + audio: options.audio ?? true, + video: options.video ?? true, + }; + + this.room = await connect(token, connectOptions); + + // 设置事件监听器 + this.setupRoomEventListeners(); + + return this.room; + } catch (error) { + console.error('Error connecting to room:', error); + throw error; + } + } + + // 断开连接 + disconnect(): void { + if (this.room) { + this.room.disconnect(); + this.room = null; + } + + if (this.localVideoTrack) { + this.localVideoTrack.stop(); + this.localVideoTrack = null; + } + + if (this.localAudioTrack) { + this.localAudioTrack.stop(); + this.localAudioTrack = null; + } + } + + // 切换音频 + toggleAudio(): boolean { + if (this.room && this.room.localParticipant) { + const audioTrack = Array.from(this.room.localParticipant.audioTracks.values())[0]; + if (audioTrack) { + if (audioTrack.track.isEnabled) { + audioTrack.track.disable(); + } else { + audioTrack.track.enable(); + } + return audioTrack.track.isEnabled; + } + } + return false; + } + + // 切换视频 + toggleVideo(): boolean { + if (this.room && this.room.localParticipant) { + const videoTrack = Array.from(this.room.localParticipant.videoTracks.values())[0]; + if (videoTrack) { + if (videoTrack.track.isEnabled) { + videoTrack.track.disable(); + } else { + videoTrack.track.enable(); + } + return videoTrack.track.isEnabled; + } + } + return false; + } + + // 获取参与者信息 + getParticipants(): ParticipantInfo[] { + if (!this.room) return []; + + const participants: ParticipantInfo[] = []; + + // 本地参与者 + const localParticipant = this.room.localParticipant; + participants.push({ + identity: localParticipant.identity, + sid: localParticipant.sid, + isLocal: true, + audioEnabled: Array.from(localParticipant.audioTracks.values()).some(track => track.track.isEnabled), + videoEnabled: Array.from(localParticipant.videoTracks.values()).some(track => track.track.isEnabled), + }); + + // 远程参与者 + this.room.participants.forEach((participant: RemoteParticipant) => { + participants.push({ + identity: participant.identity, + sid: participant.sid, + isLocal: false, + audioEnabled: Array.from(participant.audioTracks.values()).some(track => track.track && track.track.isEnabled), + videoEnabled: Array.from(participant.videoTracks.values()).some(track => track.track && track.track.isEnabled), + }); + }); + + return participants; + } + + // 获取当前房间 + getCurrentRoom(): Room | null { + return this.room; + } + + // 设置房间事件监听器 + private setupRoomEventListeners(): void { + if (!this.room) return; + + this.room.on('participantConnected', (participant: RemoteParticipant) => { + console.log('Participant connected:', participant.identity); + }); + + this.room.on('participantDisconnected', (participant: RemoteParticipant) => { + console.log('Participant disconnected:', participant.identity); + }); + + this.room.on('disconnected', (room: Room) => { + console.log('Disconnected from room:', room.name); + }); + + this.room.on('reconnecting', (error: any) => { + console.log('Reconnecting to room...', error); + }); + + this.room.on('reconnected', () => { + console.log('Reconnected to room'); + }); + } +} + +export const twilioService = new TwilioService(); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 95f8bd0..12aff7f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,6 +15,11 @@ export default defineConfig({ }, extensions: ['.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.tsx', '.ts', '.jsx', '.js'], }, + esbuild: { + // 在开发环境中忽略一些TypeScript错误 + target: 'esnext', + logOverride: { 'this-is-undefined-in-esm': 'silent' } + }, define: { // React Native Web 需要的全局变量 global: 'globalThis',