From 0fe2a93f20df0ca437e2bdb62cdaf9e90bca1771 Mon Sep 17 00:00:00 2001 From: wulong Date: Tue, 1 Jul 2025 19:10:12 +0800 Subject: [PATCH] feat: update demo(NERtcSample-1to1-Web-React) --- .../NERtcSample-1to1-Web-React/.eslintrc.js | 29 + .../NERtcSample-1to1-Web-React/.gitignore | 26 + .../NERtcSample-1to1-Web-React/.prettierrc | 18 + .../NERtcSample-1to1-Web-React/CHANGELOG.md | 28 + .../NERtcSample-1to1-Web-React/README.md | 49 ++ .../NERtcSample-1to1-Web-React/index.html | 14 + .../NERtcSample-1to1-Web-React/package.json | 49 ++ .../public/favicon.bak.ico | Bin 0 -> 1150 bytes .../public/favicon.ico | Bin 0 -> 4907 bytes .../public/vite.svg | 1 + .../src/.prettierrc | 4 + .../NERtcSample-1to1-Web-React/src/App.css | 47 ++ .../NERtcSample-1to1-Web-React/src/App.tsx | 25 + .../src/assets/camera-closed.png | Bin 0 -> 1581 bytes .../src/assets/camera-flip.png | Bin 0 -> 1804 bytes .../src/assets/camera-opned.png | Bin 0 -> 850 bytes .../src/assets/mute.png | Bin 0 -> 1848 bytes .../src/assets/openCamera.png | Bin 0 -> 2907 bytes .../src/assets/over.png | Bin 0 -> 1769 bytes .../src/assets/react.svg | 1 + .../src/assets/relieve-silence.png | Bin 0 -> 3869 bytes .../src/assets/silence.png | Bin 0 -> 2109 bytes .../src/assets/stopCamera.png | Bin 0 -> 2402 bytes .../src/assets/unmute.png | Bin 0 -> 2647 bytes .../src/assets/yunxinLogo.png | Bin 0 -> 4907 bytes .../src/assets/yunxinLogo1.png | Bin 0 -> 4222 bytes .../src/components/Pip/index.css | 9 + .../src/components/Pip/index.tsx | 196 +++++ .../src/components/loading/index.tsx | 5 + .../src/components/nav/index.css | 24 + .../src/components/nav/index.tsx | 35 + .../src/components/networkSignal/index.css | 43 + .../src/components/networkSignal/index.tsx | 64 ++ .../src/components/wechatQrCode/index.css | 141 ++++ .../src/components/wechatQrCode/index.tsx | 132 +++ .../src/config/index.tsx | 2 + .../src/constant/index.ts | 9 + .../src/features/browserVisibilityMonitor.tsx | 131 +++ .../src/features/rtcManager.tsx | 447 ++++++++++ .../src/features/rtcPreview.tsx | 261 ++++++ .../src/global.d.ts | 22 + .../src/hooks/useVisibility.ts | 26 + .../NERtcSample-1to1-Web-React/src/index.css | 69 ++ .../NERtcSample-1to1-Web-React/src/main.tsx | 11 + .../src/pages/home/index.css | 22 + .../src/pages/home/index.tsx | 120 +++ .../src/pages/preview/index.css | 198 +++++ .../src/pages/preview/index.tsx | 758 +++++++++++++++++ .../src/pages/rtc/index.css | 64 ++ .../src/pages/rtc/index.tsx | 767 ++++++++++++++++++ .../src/routes/router.tsx | 37 + .../src/store/index.tsx | 78 ++ .../src/types/index.ts | 57 ++ .../src/utils/index.tsx | 239 ++++++ .../NERtcSample-1to1-Web-React/ssh/server.crt | 21 + .../NERtcSample-1to1-Web-React/ssh/server.key | 28 + .../tsconfig.app.json | 32 + .../NERtcSample-1to1-Web-React/tsconfig.json | 23 + .../tsconfig.node.json | 30 + .../NERtcSample-1to1-Web-React/vite.config.ts | 47 ++ README.md | 1 + 61 files changed, 4440 insertions(+) create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/.eslintrc.js create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/.gitignore create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/.prettierrc create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/CHANGELOG.md create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/README.md create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/index.html create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/package.json create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/public/favicon.bak.ico create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/public/favicon.ico create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/public/vite.svg create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/.prettierrc create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/App.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/App.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/camera-closed.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/camera-flip.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/camera-opned.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/mute.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/openCamera.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/over.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/react.svg create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/relieve-silence.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/silence.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/stopCamera.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/unmute.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/yunxinLogo.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/assets/yunxinLogo1.png create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/Pip/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/Pip/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/loading/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/nav/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/nav/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/networkSignal/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/networkSignal/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/wechatQrCode/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/components/wechatQrCode/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/config/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/constant/index.ts create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/features/browserVisibilityMonitor.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/features/rtcManager.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/features/rtcPreview.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/global.d.ts create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/hooks/useVisibility.ts create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/main.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/pages/home/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/pages/home/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/pages/preview/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/pages/preview/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/pages/rtc/index.css create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/pages/rtc/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/routes/router.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/store/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/types/index.ts create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/src/utils/index.tsx create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/ssh/server.crt create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/ssh/server.key create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/tsconfig.app.json create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/tsconfig.json create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/tsconfig.node.json create mode 100644 One-to-One-Video/NERtcSample-1to1-Web-React/vite.config.ts diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/.eslintrc.js b/One-to-One-Video/NERtcSample-1to1-Web-React/.eslintrc.js new file mode 100644 index 0000000..df28d0a --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'eslint:recommended', // ESLint 推荐规则 + 'plugin:react/recommended', + 'plugin:@typescript-eslint/recommended', + 'prettier', + 'plugin:prettier/recommended', // 继承 Prettier 规则 + ], + plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], + env: { + browser: true, + es6: true, + node: false, + }, + settings: { + react: { + version: 'detect', + }, + }, + rules: { + // 自定义规则(可选) + 'react/react-in-jsx-scope': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'prettier/prettier': 'error', + '@typescript-eslint/explicit-module-boundary-types': 'off', + }, +}; diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/.gitignore b/One-to-One-Video/NERtcSample-1to1-Web-React/.gitignore new file mode 100644 index 0000000..265f50c --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +package-lock.json \ No newline at end of file diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/.prettierrc b/One-to-One-Video/NERtcSample-1to1-Web-React/.prettierrc new file mode 100644 index 0000000..fb1194e --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/.prettierrc @@ -0,0 +1,18 @@ + +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "semi": true, + "tabWidth": 2, + "jsxSingleQuote": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "auto", + "overrides": [ + { + "files": ".prettierrc", + "options": { "parser": "typescript" } + } + ] +} \ No newline at end of file diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/CHANGELOG.md b/One-to-One-Video/NERtcSample-1to1-Web-React/CHANGELOG.md new file mode 100644 index 0000000..19056fe --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +- 体验地址: https://app.yunxin.163.com/webdemo/tender/#/nertcDemoH5 + +## [1.0.0] - 2025-07-01 + +### Added + +- 完整的点对点音视频通话通话场景(移动端H5) +- 完整了会前设备检测流程 + - 系统环境检查 + - 音频采集检查 + - 扬声器检查 + - 视频采集检查 + - 网络检查检查 +- 基本音视频会控 + - 布局为大小屏模式(默认对端是大盘) + - 支持大小屏切换 + - 支持小屏滑动 + - 支持开关麦克风 + - 支持开关摄像头 + - 支持前置后置摄像头切换 +- 异常监控 + - 规避浏览器的音频自动播放限制(通过手动触发) + - 支持音视频设备不可用异常监控 + - 支持音视频设备没有数据流监控 + - 支持网络异常监控 + - 监控网络质量变化 diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/README.md b/One-to-One-Video/NERtcSample-1to1-Web-React/README.md new file mode 100644 index 0000000..54385dc --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/README.md @@ -0,0 +1,49 @@ +# 云信音视频移动端浏览器H5 Demo + +## 功能 + +- H5音视频通话会前检测功能 +- H5音视频实时通话功能(包含各种会控) + +## 结构 + +- src: 源码目录 + - main.tsx: 入口文件 + - App.tsx: 根组件,作为所有其他组件的容器 + - src/pages: 页面目录 + - src/pages/home: 首页 + - src/pages/preview: 会前检测页面 + - src/pages/rtc: 实时通话页面 + - src/components: 组件目录 + - src/config: 全局配置 + - src/constant: 全局定义 + - src/assets: 静态资源 + - src/store: 全局变量 + - src/types: 类型定义 + - src/hooks: 全局钩子 + - src/routes: 路由配置 + - src/features: 功能模块目录 + - src/utils: 工具库 +- dist: 打包目录 +- package.json: 依赖包 +- tsconfig.json: ts语法编译配置 +- .umirc.ts: 开发环境配置 + +## 开发流程 + +### 环境依赖 + +- 脚手架: umi +- 环境要求:node v18.x +- src/config/config.ts: 配置云信的appkey和secret + +### 开发环境启动 + +- npm install (node使用 v18版本) +- npm run dev + +### 打包 + +- npm run build + +### 部署 diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/index.html b/One-to-One-Video/NERtcSample-1to1-Web-React/index.html new file mode 100644 index 0000000..4d7ca78 --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/index.html @@ -0,0 +1,14 @@ + + + + + + + + NERTC H5 Demo + + +
+ + + \ No newline at end of file diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/package.json b/One-to-One-Video/NERtcSample-1to1-Web-React/package.json new file mode 100644 index 0000000..ac0d7a3 --- /dev/null +++ b/One-to-One-Video/NERtcSample-1to1-Web-React/package.json @@ -0,0 +1,49 @@ +{ + "name": "nertc-demo-h5", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-basic-ssl": "^2.0.0", + "@vitejs/plugin-react": "^4.4.1", + "antd-mobile": "^5.39.0", + "consola": "^3.4.2", + "import": "^0.0.6", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.30.1", + "vconsole": "^3.15.1" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.15.21", + "@types/react": "18.2.0", + "@types/react-copy-to-clipboard": "^5.0.7", + "@types/react-dom": "18.2.0", + "antd": "^5.25.2", + "classnames": "2.3.2", + "eslint": "^9.28.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.4.1", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^16.0.0", + "js-sha1": "^0.7.0", + "lodash-es": "^4.17.21", + "nertc-web-sdk": "^5.8.20", + "path": "^0.12.7", + "prettier": "^3.5.3", + "qrcode.react": "^4.2.0", + "react-copy-to-clipboard": "^5.1.0", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5" + } +} diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/public/favicon.bak.ico b/One-to-One-Video/NERtcSample-1to1-Web-React/public/favicon.bak.ico new file mode 100644 index 0000000000000000000000000000000000000000..f41aa60c9d707e229c4392db51c621abd7e05e31 GIT binary patch literal 1150 zcma))y-QnR6vj_W2pNioBJn#(lMaqfMT+1*;N;M$e?aLdPJyH1#6*m*zmxmk+uKg}h9~E}=RMDJzOKjve(|{QpO#k~p=xM&RyXy?(!O9`Lx=tEnkx zanPy1iJQxn(JmUMT~3?UYV!B>S~^MWEc_l9-80^GeZ2?&WT%{|w@OL4Bi%pC!>`+G z=^ST+ra>zY=cUbkfgK8PC*c27Z8W-Ip;<~vvp6P=_laDqG_ETmAJX9!l+Tum+nhHJ2#g4;pO-y+;^9|Z@HR(k=~-r_JG{l zkNUn4@6XRl>74c3@aIe(f6nBM!9C!Y;eFe?w_JbDnu9y->(GnAeg_HW z^pHJm+S*hff5zAL>hmb`7$e$7E%@C-i|)L`GnSsF6z?VLURQ*)3P zJky72%wwzA57b@)PlGYu`SzVi8#Iha4V;{a6!Ib)d`Vl=V2X1CU(LO%6^QMJlM=a` G5xEC8l!oU3 literal 0 HcmV?d00001 diff --git a/One-to-One-Video/NERtcSample-1to1-Web-React/public/favicon.ico b/One-to-One-Video/NERtcSample-1to1-Web-React/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..be8cbcbfb01a49949d3383fc8255f488caec388f GIT binary patch literal 4907 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 d(Ve!6;IGNMtg>kYlm!3)002ovPDHLkV1jJy8