init live
287
public/CHANGELOG.md
Normal file
@ -0,0 +1,287 @@
|
||||
# TRTC Web SDK 版本发布日志
|
||||
|
||||
- 版本号规则:[major.minor.patch]
|
||||
- major:主版本号,如有重大版本重构则该字段递增,通常各主版本间接口不兼容。
|
||||
- minor:次版本号,各次版本号间接口保持兼容,如有接口新增或优化则该字段递增。
|
||||
- patch:补丁号,如有功能改善或缺陷修复则该字段递增。
|
||||
- 我们建议您及时更新到最新版本以便获得更好的产品稳定性及在线支持!
|
||||
|
||||
## 4.6.6 (2020-10-23)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 优化上行 peerConnection 重连逻辑
|
||||
- 优化下行 peerConnection 重连逻辑
|
||||
- 优化 TRTC.checkSystemRequirements 检测逻辑
|
||||
- 支持 Safari 屏幕分享,参考:[屏幕分享使用教程](https://trtc-1252463788.file.myqcloud.com/web/docs/tutorial-06-advanced-screencast.html)
|
||||
|
||||
**Bug Fixed**
|
||||
|
||||
- 修复因自动播放策略限制,手动恢复音频播放后,getAudioLevel 值为0的问题
|
||||
|
||||
## 4.6.5(2020-10-14)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 优化 WebSocket 信令通道重连逻辑,提升连接稳定性
|
||||
- 优化日志输出逻辑
|
||||
|
||||
**Bug Fixed**
|
||||
|
||||
- Chrome 重新订阅后,getAudioLevel 接口返回值为0的问题
|
||||
- Safari 重新订阅后,播放无声的问题
|
||||
- 使用 replaceTrack 替换上行音频轨道后,getLocalVideoStats 接口返回 undefined 的问题
|
||||
- 移动设备通话过程中,切换网络类型,偶现 WebSocket 连接断开的问题
|
||||
|
||||
## 4.6.4(2020-9-24)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 退房后停止网络质量统计
|
||||
|
||||
**Bug Fixed**
|
||||
|
||||
- 修复 Chrome 56 进房报错的问题
|
||||
- 修复移动端推旁路出现画面旋转的问题
|
||||
- 修复纯音频推流时云端录制异常的问题
|
||||
- 修复因分辨率不一致导致摄像头拔出后,自动恢复推流失败的问题
|
||||
|
||||
## 4.6.3 (2020-8-28)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 优化兼容性检测逻辑
|
||||
- 优化日志上报逻辑
|
||||
- 优化上行码率控制逻辑
|
||||
|
||||
## 4.6.2 (2020-8-14)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 优化上行码率调控逻辑
|
||||
- 优化 switchRole 参数校验逻辑
|
||||
- 优化上行网络质量计算逻辑
|
||||
- 优化错误提示信息
|
||||
- 检测当前推流采集设备变更时,自动恢复推流状态
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复 unpublish 成功后,立即重新 publish 失败报错的问题
|
||||
|
||||
## 4.6.1 (2020-7-28)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- [TRTC.isScreenShareSupported](https://trtc-1252463788.file.myqcloud.com/web/docs/TRTC.html#.isScreenShareSupported) Safari 不支持屏幕分享
|
||||
- 完善 subscribe & unsubscribe 接口的参数校验逻辑
|
||||
- 增加网络质量日志
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复当未授权媒体设备,且 TRTC.createStream 接口传入的设备 ID 为空串时,SDK 报 OverconstrainedError 的问题
|
||||
- 修复上行 peerConnection 断开时没有打印日志的问题
|
||||
|
||||
## 4.6.0 (2020-7-16)
|
||||
|
||||
**Feature**
|
||||
|
||||
- 增加 NETWORK_QUALITY 事件
|
||||
|
||||
## 4.5.0 (2020-7-2)
|
||||
|
||||
**Feature**
|
||||
|
||||
- createStream 接口增加 screenAudio 参数
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复 Android 浏览器中回声消除不起作用的问题
|
||||
- 修复 getTransportStats 接口返回的 rtt 值为 NAN 的问题
|
||||
|
||||
## 4.4.0 (2020-5-28)
|
||||
|
||||
**Feature**
|
||||
|
||||
- 支持 Chrome >= 74 屏幕分享采集系统(windows)或者当前 Tab 页面(Mac)的声音
|
||||
|
||||
## 4.3.14 (2020-4-29)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复小程序音频 muted unmute 事件。
|
||||
|
||||
## 4.3.13 (2020-4-13)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 优化浏览器可用性检测
|
||||
|
||||
## 4.3.12 (2020-4-13)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复一个潜在的RTCPeerConnection状态变化异常
|
||||
|
||||
## 4.3.11 (2020-3-28)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加手机 QQ 浏览器检测,手机 QQ 浏览器暂时无法支持 WebRTC
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复 Boolean 返回值类型
|
||||
|
||||
## 4.3.10 (2020-3-17)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 优化环境检测逻辑
|
||||
- RtcError 增加 name code
|
||||
|
||||
## 4.3.9 (2020-3-13)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加部署环境自动检测
|
||||
- 优化日志
|
||||
|
||||
## 4.3.8 (2020-2-24)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- createClient 增加 streamId userdefinerecordid 字段
|
||||
|
||||
## 4.3.7 (2020-2-21)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 屏幕分享时切换设备抛出异常。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 切换设备时释放 MediaStream,解决设备占用问题。
|
||||
- 订阅接口增加处理潜在错误。
|
||||
|
||||
## 4.3.6 (2020-2-5)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 调整 Stream.resume() 音视频播放顺序,修复 iOS 上微信浏览器自动播放异常问题。
|
||||
|
||||
## 4.3.5 (2020-2-5)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加 publish 超时检查,提高信令发送成功率。
|
||||
|
||||
## 4.3.4 (2020-1-6)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 升级 core-js 至 v3.6.1。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- unpublish 超时后向外部抛出异常事件。
|
||||
- 修复第三方库引起 V8 负优化问题。
|
||||
|
||||
## 4.3.3 (2019-12-25)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加主动检测环境是否支持 webrtc 能力。
|
||||
- 优化 sdp 响应机制。
|
||||
- 优化上报逻辑。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复 turn url 协议格式。
|
||||
|
||||
## 4.3.2 (2019-12-09)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加下行连接 ICE 断开自动重连机制。
|
||||
- 去除 STUN 打洞环节,增加内网用户连接成功率及提高连接速度。
|
||||
- 日志上报时间戳统一使用服务器校正后的 UTC 时间。
|
||||
- 优化 ICE 错误上报。
|
||||
- 增加更多关键事件上报到 avmonitor 监控。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复 WebSocket 信令通道 1005 异常重连及重连错误处理。
|
||||
- 修复下行丢包率上报问题。
|
||||
|
||||
## 4.3.1 (2019-11-23)
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加通话过程中上行链路 ICE 断开自动重连机制。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复 STUN 打洞失败后 host 公网 IP 类型 ICE Candidate 不生效问题。
|
||||
|
||||
## 4.3.0(2019-11-15)
|
||||
|
||||
**Feature**
|
||||
|
||||
增加 Client.getTransportStats() API。
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 增加更详细的上报日志。
|
||||
- 事件解除绑定支持通配符。
|
||||
- 增加连接超时时间至 5s。
|
||||
- 增加发布超时时间至 5s。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
修复因 zone.js 修改原型链导致 SDK 判断异常的问题。
|
||||
|
||||
## 4.2.0(2019-11-04)
|
||||
|
||||
**Feature**
|
||||
|
||||
- 增加 Client.off() 接口取消客户端事件绑定。
|
||||
|
||||
**Improvement**
|
||||
|
||||
- 通话状态统计优化。
|
||||
- Client.publish() 增加权限检查。
|
||||
- Stream.play()/resume() 增加自动播放错误提示。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- LocalStream.switchDevice() 切换摄像头黑屏问题修复。
|
||||
|
||||
## 4.1.1(2019-10-24)
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复日志丢失问题。
|
||||
- 修复断网重连远端用户丢失问题。
|
||||
|
||||
## 4.1.0(2019-10-17)
|
||||
|
||||
**Feature**
|
||||
|
||||
- Stream.play() 接口支持传入 HTMLDivElement 对象。
|
||||
- 增加音频码率调控设置,开发者可通过 LocalStream.setAudioProfile() 设置音频属性,目前支持两种 Profile:standard 和 high。
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
- 修复旧版本 Chrome 上的 WebAudio Context 数量受限问题。
|
||||
- 修复 replaceTrack() 未重启本地音视频播放器问题。
|
||||
- 修复 LocalStream.setScreenProfile() 自定义属性设置未生效问题。
|
||||
- 修复 audio/video player 重启及状态上报问题。
|
||||
|
||||
## 4.0.0(2019-10-11)
|
||||
|
||||
TRTC Web SDK (WebRTC) 重构版本,提供 Client/Stream 模式的接口,各对象职责更明确,语义更简洁明了。
|
||||
重构版本与旧版本不兼容,除接口改动之外,还提供如下功能:
|
||||
|
||||
- 视频属性 (分辨率、帧率及码率)控制完全由 App 通过 SDK 的 LocalStream.setVideoProfile() 接口设置,不再支持老版本通过腾讯云控制台的“画面设定 (Spear Role)”。
|
||||
- SDK 在 Stream 对象中封装了音视频播放器,音视频播放完全由 SDK 控制。
|
||||
- 提供远端流的订阅与取消订阅功能,开发者可以通过 Client.subscribe()/unsubscribe() 接口灵活控制远端流的音频、视频或音视频数据流的接收。
|
98
public/README.md
Normal file
@ -0,0 +1,98 @@
|
||||
本文主要介绍如何快速运行腾讯云 TRTC Web SDK Demo。
|
||||
|
||||
## 支持的平台
|
||||
|
||||
WebRTC 技术由 Google 最先提出,目前主要在桌面版 Chrome 浏览器、桌面版 Safari 浏览器以及移动版的 Safari 浏览器上有较为完整的支持,其他平台(例如 Android 平台的浏览器)支持情况均比较差。
|
||||
- 在移动端推荐使用 [小程序](https://cloud.tencent.com/document/product/647/32399) 解决方案,微信和手机 QQ 小程序均已支持,都是由各平台的 Native 技术实现,音视频性能更好,且针对主流手机品牌进行了定向适配。
|
||||
- 如果您的应用场景主要为教育场景,那么教师端推荐使用稳定性更好的 [Electron](https://cloud.tencent.com/document/product/647/38549) 解决方案,支持大小双路画面,更灵活的屏幕分享方案以及更强大而弱网络恢复能力。
|
||||
|
||||
| 操作系统 | 浏览器类型 | 浏览器最低版本要求 | 接收(播放) | 发送(上麦) | 屏幕分享 |
|
||||
| :------: | :------------------: | :----------------: | :----------: | :----------: | :-----------------------: |
|
||||
| Mac OS | 桌面版 Safari 浏览器 | 11+ | 支持 | 支持 | 不支持 |
|
||||
| Mac OS | 桌面版 Chrome 浏览器 | 56+ | 支持 | 支持 | 支持(需要chrome72+版本) |
|
||||
| Windows | 桌面版 Chrome 浏览器 | 56+ | 支持 | 支持 | 支持(需要chrome72+版本) |
|
||||
| Windows | 桌面版 QQ 浏览器 | 10.4 | 支持 | 支持 | 不支持 |
|
||||
| iOS | 移动版 Safari 浏览器 | 11.1.2 | 支持 | 支持 | 不支持 |
|
||||
| iOS | 微信内嵌网页 | 12.1.4 | 支持 | 不支持 | 不支持 |
|
||||
| Android | 移动版 QQ 浏览器 | - | 不支持 | 不支持 | 不支持 |
|
||||
| Android | 移动版 UC 浏览器 | - | 不支持 | 不支持 | 不支持 |
|
||||
| Android | 微信内嵌网页 | - | 不支持 | 不支持 | 不支持 |
|
||||
|
||||
>!
|
||||
>- 您可以在浏览器中打开 [WebRTC 能力测试](https://www.qcloudtrtc.com/webrtc-samples/abilitytest/index.html) 页面进行检测是否完整支持 WebRTC。例如公众号等浏览器环境。
|
||||
>- 由于 H.264 版权限制,华为系统的 Chrome 浏览器和以 Chrome WebView 为内核的浏览器均不支持 TRTC 的 Web 版 SDK 的正常运行。
|
||||
|
||||
<span id="requirements"></span>
|
||||
## 环境要求
|
||||
- 请使用最新版本的 Chrome 浏览器。
|
||||
- TRTC Web SDK 依赖以下端口进行数据传输,请将其加入防火墙白名单,配置完成后,您可以通过访问并体验 [官网 Demo](https://trtc-1252463788.file.myqcloud.com/web/demo/official-demo/index.html) 检查配置是否生效。
|
||||
- TCP 端口:8687
|
||||
- UDP 端口:8000;8080;8800;843;443;16285
|
||||
- 域名:qcloud.rtc.qq.com
|
||||
|
||||
## 前提条件
|
||||
您已 [注册腾讯云](https://cloud.tencent.com/document/product/378/17985) 账号,并完成 [实名认证](https://cloud.tencent.com/document/product/378/3629)。
|
||||
|
||||
## 操作步骤
|
||||
<span id="step1"></span>
|
||||
### 步骤1:创建新的应用
|
||||
1. 登录实时音视频控制台,选择【开发辅助】>【[快速跑通Demo](https://console.cloud.tencent.com/trtc/quickstart)】。
|
||||
2. 单击【立即开始】,输入应用名称,例如`TestTRTC`,单击【创建应用】。
|
||||
|
||||
<span id="step2"></span>
|
||||
### 步骤2:下载 SDK 和 Demo 源码
|
||||
1. 鼠标移动至对应卡片,单击【[Github](https://github.com/tencentyun/TRTCSDK/tree/master/Web/TRTCSimpleDemo)】跳转至 Github(或单击【[ZIP](https://liteavsdk-1252463788.cos.ap-guangzhou.myqcloud.com/H5_latest.zip?_ga=1.195966252.185644906.1567570704)】),下载相关 SDK 及配套的 Demo 源码。
|
||||
![](https://main.qcloudimg.com/raw/0f35fe3bafe9fcdbd7cc73f991984d1a.png)
|
||||
2. 下载完成后,返回实时音视频控制台,单击【我已下载,下一步】,可以查看 SDKAppID 和密钥信息。
|
||||
|
||||
<span id="step3"></span>
|
||||
### 步骤3:配置 Demo 工程文件
|
||||
1. 解压 [步骤2](#step2) 中下载的源码包。
|
||||
2. 找到并打开`Web/TRTCSimpleDemo/js/debug/GenerateTestUserSig.js`文件。
|
||||
3. 设置`GenerateTestUserSig.js`文件中的相关参数:
|
||||
<ul><li>SDKAPPID:默认为0,请设置为实际的 SDKAppID。</li>
|
||||
<li>SECRETKEY:默认为空字符串,请设置为实际的密钥信息。</li></ul>
|
||||
<img src="https://main.qcloudimg.com/raw/1732ea2401af6111b41259a78b5330a4.png">
|
||||
4. 返回实时音视频控制台,单击【粘贴完成,下一步】。
|
||||
5. 单击【关闭指引,进入控制台管理应用】。
|
||||
|
||||
>!本文提到的生成 UserSig 的方案是在客户端代码中配置 SECRETKEY,该方法中 SECRETKEY 很容易被反编译逆向破解,一旦您的密钥泄露,攻击者就可以盗用您的腾讯云流量,因此**该方法仅适合本地跑通 Demo 和功能调试**。
|
||||
>正确的 UserSig 签发方式是将 UserSig 的计算代码集成到您的服务端,并提供面向 App 的接口,在需要 UserSig 时由您的 App 向业务服务器发起请求获取动态 UserSig。更多详情请参见 [服务端生成 UserSig](https://cloud.tencent.com/document/product/647/17275#Server)。
|
||||
|
||||
### 步骤4:运行 Demo
|
||||
使用 Chrome 浏览器打开 Demo 根目录下的`index.html`文件即可运行 Demo。
|
||||
|
||||
>!
|
||||
> - 一般情况下体验 Demo 需要部署至服务器,通过`https://域名/xxx`访问,或者直接在本地搭建服务器,通过`localhost:端口`访问。
|
||||
> - 目前桌面端 Chrome 浏览器支持 TRTC Web SDK 的相关特性比较完整,因此建议使用 Chrome 浏览器进行体验。
|
||||
|
||||
Demo 运行界面如图所示:
|
||||
![](https://main.qcloudimg.com/raw/e989c968446e6e3bdcc19c58e40e2b86.png)
|
||||
- 单击【加入房间】加入音视频通话房间并且发布本地音视频流。
|
||||
您可以打开多个页面,每个页面都单击 【加入房间】,正常情况下可以看到多个画面并模拟实时音视频通话。
|
||||
- 单击摄像头图标可以选择摄像头设备。
|
||||
- 单击麦克风图表可以选择麦克风设备。
|
||||
|
||||
>?WebRTC 需要使用摄像头和麦克风采集音视频,在体验过程中您可能会收到来自 Chrome 浏览器的相关提示,单击【允许】。
|
||||
> ![](https://main.qcloudimg.com/raw/1a2c1e7036720b11f921f8ee1829762a.png)
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 查看密钥时只能获取公钥和私钥信息,要如何获取密钥?
|
||||
TRTC SDK 6.6 版本(2019年08月)开始启用新的签名算法 HMAC-SHA256。在此之前已创建的应用,需要先升级签名算法才能获取新的加密密钥。如不升级,您也可以继续使用 [老版本算法 ECDSA-SHA256](https://cloud.tencent.com/document/product/647/17275#.E8.80.81.E7.89.88.E6.9C.AC.E7.AE.97.E6.B3.95)。
|
||||
|
||||
升级操作:
|
||||
1. 登录 [实时音视频控制台](https://console.cloud.tencent.com/trtc)。
|
||||
2. 在左侧导航栏选择【应用管理】,单击目标应用所在行的【应用信息】。
|
||||
3. 选择【快速上手】页签,单击【第二步 获取签发UserSig的密钥】区域的【点此升级】。
|
||||
|
||||
### 2. 出现客户端错误:“RtcError: no valid ice candidate found”该如何处理?
|
||||
出现该错误说明 TRTC Web SDK 在 STUN 打洞失败,请根据 [环境要求](#requirements) 检查防火墙配置。
|
||||
|
||||
### 3. 出现客户端错误:"RtcError: ICE/DTLS Transport connection failed" 或 “RtcError: DTLS Transport connection timeout”该如何处理?
|
||||
出现该错误说明 TRTC Web SDK 在建立媒体传输通道时失败,请根据 [环境要求](#requirements) 检查防火墙配置。
|
||||
|
||||
### 4. 出现10006 error 该如何处理?
|
||||
如果出现"Join room failed result: 10006 error: service is suspended,if charge is overdue,renew it",请确认您的实时音视频应用的服务状态是否为可用状态。
|
||||
登录 [实时音视频控制台](https://console.cloud.tencent.com/rav),单击您创建的应用,单击【帐号信息】,在帐号信息面板即可确认服务状态。
|
||||
![](https://main.qcloudimg.com/raw/13c9b520ea333804cffb4e2c4273fced.png)
|
8
public/css/bootstrap-material-design.min.css
vendored
Normal file
530
public/css/index.css
Normal file
@ -0,0 +1,530 @@
|
||||
*{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body{
|
||||
font-family: PingFangSC-Regular !important;
|
||||
}
|
||||
html{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.icon {
|
||||
width: 1em; height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
button{
|
||||
margin: 0 !important;
|
||||
color: #fff !important;
|
||||
background-color: #006EFF !important
|
||||
}
|
||||
div{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.row-div{
|
||||
flex-direction: row;
|
||||
}
|
||||
.col-div{
|
||||
flex-direction: column;
|
||||
}
|
||||
#root{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgb(250, 250, 250);
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
#login-root{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#login-card{
|
||||
width: 450px;
|
||||
height: 500px;
|
||||
display: flex;
|
||||
/* justify-content: center; */
|
||||
align-items: center;
|
||||
background-color: white;
|
||||
}
|
||||
.login-card{
|
||||
width: 360px;
|
||||
height: 450px;
|
||||
background-color: white;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
/* justify-content: center; */
|
||||
align-items: center;
|
||||
}
|
||||
.popover{
|
||||
min-width: 300px;
|
||||
max-width: 1000px;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
.popover-body{
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
.popover-body>div{
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
}
|
||||
.popover-body>div:hover{
|
||||
background-color: #F7F7F7;
|
||||
}
|
||||
|
||||
.icon-gray{
|
||||
color: #bfbfbf;
|
||||
}
|
||||
.icon-normal{
|
||||
color: #515151;
|
||||
}
|
||||
.icon-blue{
|
||||
color: #006EFF;
|
||||
}
|
||||
.device-testing-btn{
|
||||
color: #515151;
|
||||
cursor: pointer;
|
||||
margin-top: -14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.device-connect-list{
|
||||
width: 310px;
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
justify-content: space-around;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.device-connect-list::before{
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 8px transparent solid;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
bottom: -16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.device-connect-list::after{
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 7px transparent solid;
|
||||
border-top-color: #fff;
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.connect{
|
||||
width: 28px;
|
||||
height: 64px;
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.device-icon{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 3px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
#device-testing-root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.device-testing-card{
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
background: #F7F7F7;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.device-testing-prepare,.device-testing,.device-testing-report{
|
||||
display: block;
|
||||
}
|
||||
.testing-title{
|
||||
font-size: 34px;
|
||||
justify-content: center;
|
||||
margin-top: 55px;
|
||||
color: #201e1ee5;
|
||||
}
|
||||
.testing-prepare-info{
|
||||
font-size: 16px;
|
||||
justify-content: center;
|
||||
margin-top: 25px;
|
||||
color: #585656e5;
|
||||
}
|
||||
.device-testing-close-btn{
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
.device-display{
|
||||
margin-top: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
.device{
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
font-size: 38px;
|
||||
}
|
||||
.device:not(:first-child){
|
||||
margin-left: 60px;
|
||||
}
|
||||
.device:not(:first-child).safari{
|
||||
margin-left: 100px;
|
||||
}
|
||||
.device::before{
|
||||
content: '';
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
bottom: -34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.connect-success::before{
|
||||
background: url('../img/success.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.connect-fail::before{
|
||||
background: url('../img/fail.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
@keyframes device-loading {
|
||||
0%{
|
||||
width: 0%;
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
50%{
|
||||
width: 50%;
|
||||
border-radius: 6px 0 0 6px;
|
||||
}
|
||||
100%{
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
.loading-background{
|
||||
width: 350px;
|
||||
height: 3px;
|
||||
border-radius: 6px;
|
||||
margin: 20px auto 0;
|
||||
background: #bfbfbf;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.device-loading{
|
||||
height: 3px;
|
||||
background-color: #808080;
|
||||
animation: device-loading 2s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
.connect-info{
|
||||
margin-top: 60px;
|
||||
display: flex;
|
||||
height: 48px;
|
||||
justify-content: center;
|
||||
}
|
||||
.connect-attention-container{
|
||||
position: relative;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.connect-attention-icon{
|
||||
font-size: 20px;
|
||||
color: red;
|
||||
}
|
||||
.connect-attention-info{
|
||||
padding: 8px 12px;
|
||||
min-width: 120px;
|
||||
min-height: 50px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
transform: translate(20px, -10px);
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
.connect-attention-info::after{
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 10px transparent solid;
|
||||
border-top-color: rgba(0, 0, 0, 0.6);
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 100%;
|
||||
transform: translateX(-40px);
|
||||
}
|
||||
.testing-btn-display{
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.test-btn{
|
||||
width: 200px;
|
||||
height: 44px;
|
||||
background: #006EFF;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.start-gray{
|
||||
background: #dddddd;
|
||||
color: #fff;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.device-testing-title{
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.testing{
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.testing:not(:first-child){
|
||||
margin-left: 90px;
|
||||
}
|
||||
.testing:not(:first-child)::before {
|
||||
content: '';
|
||||
width: 70px;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -80px;
|
||||
background: #bfbfbf;
|
||||
}
|
||||
.testing:not(:first-child).safari{
|
||||
margin-left: 150px;
|
||||
}
|
||||
.testing:not(:first-child).safari::before{
|
||||
width: 130px;
|
||||
left: -140px;
|
||||
}
|
||||
.testing.complete {
|
||||
cursor: pointer;
|
||||
}
|
||||
.testing.complete:not(:first-child)::before {
|
||||
background: #006EFF;
|
||||
}
|
||||
.testing-body{
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.device-list{
|
||||
margin-left: 140px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.device-select{
|
||||
width: 260px;
|
||||
height: 30px;
|
||||
margin-left: 20px;
|
||||
padding: 0 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.camera-video{
|
||||
width: 300px;
|
||||
height: 180px;
|
||||
display: block;
|
||||
margin: 30px auto 0;
|
||||
}
|
||||
.testing-info-container{
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
margin-top: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.testing-info{
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
.button-list{
|
||||
margin-top: 20px;
|
||||
width: 300px;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.fail-button{
|
||||
border: 1px solid #006EFF;
|
||||
border-radius: 8px;
|
||||
color: #006EFF;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.success-button{
|
||||
border: 1px solid #006EFF;
|
||||
border-radius: 8px;
|
||||
background: #006EFF;
|
||||
color: #fff;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.audio-control{
|
||||
width: 320px;
|
||||
display: block;
|
||||
margin: 40px auto 0;
|
||||
}
|
||||
.audio-control-info{
|
||||
margin: 0px auto 20px 6px;
|
||||
color: #5f5f5f;
|
||||
}
|
||||
.mic-testing-container{
|
||||
display: block;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.mic-testing-info{
|
||||
margin-left: 140px;
|
||||
color: #bbbbbb;
|
||||
font-size: 14px;
|
||||
}
|
||||
.mic-bar-container{
|
||||
justify-content: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.mic-bar{
|
||||
width: 10px;
|
||||
height: 30px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.mic-bar:not(:first-child){
|
||||
margin-left: 3px;
|
||||
}
|
||||
.mic-bar.active{
|
||||
background: #006EFF;
|
||||
}
|
||||
.testing-index-list{
|
||||
margin-top: 40px;
|
||||
display: block;
|
||||
}
|
||||
.testing-index-group{
|
||||
width: 55%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 14px;
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
@keyframes loading-circle{
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.network-loading::before{
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('../img/loading.png');
|
||||
background-size: 100% 100%;
|
||||
animation: loading-circle 2s linear infinite;
|
||||
}
|
||||
.testing-footer{
|
||||
margin-top: 70px;
|
||||
justify-content: center;
|
||||
}
|
||||
.device-report-list{
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.device-report{
|
||||
width: 60%;
|
||||
margin: 20px auto 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.report-icon{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 20px;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
line-height: 22px;
|
||||
color: #515151;
|
||||
}
|
||||
.device-name{
|
||||
width: 280px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.device-report-footer{
|
||||
margin-top: 50px;
|
||||
justify-content: center;
|
||||
}
|
||||
.device-report-btn{
|
||||
width: 160px;
|
||||
height: 40px;
|
||||
border: 1px solid;
|
||||
border-radius: 6px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.testing-agin{
|
||||
border-color: #006EFF;
|
||||
color: #006EFF;
|
||||
}
|
||||
.testing-finish{
|
||||
background: #006EFF;
|
||||
color: #fff;
|
||||
margin-left: 60px;
|
||||
}
|
92
public/css/room.css
Normal file
@ -0,0 +1,92 @@
|
||||
*{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
font-family: Futura,sans-serif;
|
||||
}
|
||||
video{
|
||||
background-color: #d8d8d8;
|
||||
}
|
||||
/* @media screen and (min-width:960px) {
|
||||
video{
|
||||
width: auto !important;
|
||||
height: 100% !important;
|
||||
position: inherit !important;
|
||||
}
|
||||
.video-div{
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width:960px) {
|
||||
video{
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
position: inherit !important;
|
||||
}
|
||||
.video-div{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
} */
|
||||
#room-root{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 1500px;
|
||||
min-height: 700px;
|
||||
display: none;
|
||||
align-items: flex-start;
|
||||
background-color: #f0f0f0
|
||||
}
|
||||
.member{
|
||||
cursor: default;
|
||||
border-bottom-style: solid;
|
||||
border-width: 1px;
|
||||
border-bottom-color: #f0f0f0;
|
||||
}
|
||||
#video-grid{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
grid-template-areas: 'm m m a b'
|
||||
'm m m c d'
|
||||
'e f g h i';
|
||||
}
|
||||
#video-grid>div{
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
}
|
||||
#main-video{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
grid-area: 1/1/3/4;
|
||||
}
|
||||
.video-box{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.mask{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
color: #888888;
|
||||
z-index: 9;
|
||||
justify-content: center
|
||||
}
|
||||
div[id^=player] {
|
||||
background-color: #d8d8d8 !important;
|
||||
}
|
BIN
public/img/big-camera-off.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
public/img/big-camera-on.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/img/big-mic-off.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/img/big-mic-on.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/img/camera-max.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
public/img/camera-off.png
Normal file
After Width: | Height: | Size: 794 B |
BIN
public/img/camera-on.png
Normal file
After Width: | Height: | Size: 774 B |
BIN
public/img/camera.png
Normal file
After Width: | Height: | Size: 796 B |
BIN
public/img/code.jpg
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
public/img/fail.png
Normal file
After Width: | Height: | Size: 533 B |
BIN
public/img/loading.png
Normal file
After Width: | Height: | Size: 701 B |
BIN
public/img/logo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/img/logout.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
public/img/mic-off.png
Normal file
After Width: | Height: | Size: 800 B |
BIN
public/img/mic-on.png
Normal file
After Width: | Height: | Size: 761 B |
BIN
public/img/mic.png
Normal file
After Width: | Height: | Size: 809 B |
BIN
public/img/screen-off.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/img/screen-on.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/img/shot.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/img/success.png
Normal file
After Width: | Height: | Size: 569 B |
6939
public/js/bootstrap-material-design.js
vendored
Normal file
660
public/js/common.js
Normal file
@ -0,0 +1,660 @@
|
||||
/* eslint-disable no-cond-assign */
|
||||
/* global $ TRTC presetting RtcClient ShareClient */
|
||||
/* eslint-disable require-jsdoc */
|
||||
let isCamOn = true;
|
||||
let isMicOn = true;
|
||||
let isScreenOn = false;
|
||||
let isJoined = true;
|
||||
let rtc = null;
|
||||
let share = null;
|
||||
let shareUserId = '';
|
||||
let cameraId = '';
|
||||
let micId = '';
|
||||
|
||||
function login() {
|
||||
if ($('#userId').val() == '') {
|
||||
alert('用户名不能为空!');
|
||||
return;
|
||||
}
|
||||
if ($('#roomId').val() == '') {
|
||||
alert('房间号不能为空!');
|
||||
return;
|
||||
}
|
||||
presetting.login(false, options => {
|
||||
rtc = new RtcClient(options);
|
||||
join();
|
||||
});
|
||||
presetting.login(true, options => {
|
||||
shareUserId = options.userId;
|
||||
share = new ShareClient(options);
|
||||
});
|
||||
}
|
||||
|
||||
function join() {
|
||||
rtc.join();
|
||||
$('#login-root').hide();
|
||||
$('#room-root').show();
|
||||
$('#header-roomId').html('房间号: ' + $('#roomId').val());
|
||||
$('#member-me')
|
||||
.find('.member-id')
|
||||
.html($('#userId').val() + '(我)');
|
||||
}
|
||||
|
||||
function leave() {
|
||||
$('#mask_main').appendTo($('#main-video'));
|
||||
rtc.leave();
|
||||
share.leave();
|
||||
}
|
||||
|
||||
function publish() {
|
||||
rtc.publish();
|
||||
}
|
||||
|
||||
function unpublish() {
|
||||
rtc.unpublish();
|
||||
}
|
||||
|
||||
function muteAudio() {
|
||||
rtc.muteLocalAudio();
|
||||
}
|
||||
|
||||
function unmuteAudio() {
|
||||
rtc.unmuteLocalAudio();
|
||||
}
|
||||
|
||||
function muteVideo() {
|
||||
$('#mask_main').show();
|
||||
rtc.muteLocalVideo();
|
||||
}
|
||||
|
||||
function unmuteVideo() {
|
||||
rtc.unmuteLocalVideo();
|
||||
$('#mask_main').hide();
|
||||
}
|
||||
|
||||
function startSharing() {
|
||||
share.join();
|
||||
}
|
||||
|
||||
function stopSharing() {
|
||||
share.leave();
|
||||
}
|
||||
|
||||
function setBtnClickFuc() {
|
||||
//userid roomid规格
|
||||
//$('#userId').on('input', function(e) {
|
||||
// e.preventDefault();
|
||||
// console.log('userId input ' + e.target.value);
|
||||
// let val = $('#userId').val().slice(5);
|
||||
// $('#userId').val('user_'+val.replace(/[^\d]/g,''));
|
||||
//});
|
||||
$('#roomId').on('input', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('roomId input ' + e.target.value);
|
||||
let val = $('#roomId').val();
|
||||
$('#roomId').val(val.replace(/[^\d]/g, ''));
|
||||
});
|
||||
//login
|
||||
$('#login-btn').click(() => {
|
||||
login();
|
||||
});
|
||||
//open or close camera
|
||||
$('#video-btn').on('click', () => {
|
||||
if (isCamOn) {
|
||||
$('#video-btn').attr('src', './img/big-camera-off.png');
|
||||
$('#video-btn').attr('title', '打开摄像头');
|
||||
$('#member-me')
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-off.png');
|
||||
isCamOn = false;
|
||||
muteVideo();
|
||||
} else {
|
||||
$('#video-btn').attr('src', './img/big-camera-on.png');
|
||||
$('#video-btn').attr('title', '关闭摄像头');
|
||||
$('#member-me')
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-on.png');
|
||||
isCamOn = true;
|
||||
unmuteVideo();
|
||||
}
|
||||
});
|
||||
//open or close microphone
|
||||
$('#mic-btn').on('click', () => {
|
||||
if (isMicOn) {
|
||||
$('#mic-btn').attr('src', './img/big-mic-off.png');
|
||||
$('#mic-btn').attr('title', '打开麦克风');
|
||||
$('#member-me')
|
||||
.find('.member-audio-btn')
|
||||
.attr('src', 'img/mic-off.png');
|
||||
isMicOn = false;
|
||||
muteAudio();
|
||||
} else {
|
||||
$('#mic-btn').attr('src', './img/big-mic-on.png');
|
||||
$('#mic-btn').attr('title', '关闭麦克风');
|
||||
$('#member-me')
|
||||
.find('.member-audio-btn')
|
||||
.attr('src', 'img/mic-on.png');
|
||||
isMicOn = true;
|
||||
unmuteAudio();
|
||||
}
|
||||
});
|
||||
//share screen or not
|
||||
$('#screen-btn').on(
|
||||
'click',
|
||||
throttle(() => {
|
||||
if (!TRTC.isScreenShareSupported()) {
|
||||
alert('当前浏览器不支持屏幕分享!');
|
||||
return;
|
||||
}
|
||||
if ($('#screen-btn').attr('src') == './img/screen-on.png') {
|
||||
$('#screen-btn').attr('src', './img/screen-off.png');
|
||||
stopSharing();
|
||||
isScreenOn = false;
|
||||
} else {
|
||||
$('#screen-btn').attr('src', './img/screen-on.png');
|
||||
startSharing();
|
||||
isScreenOn = true;
|
||||
}
|
||||
}, 2000)
|
||||
);
|
||||
//logout
|
||||
$('#logout-btn').on('click', () => {
|
||||
leave();
|
||||
$('#room-root').hide();
|
||||
$('#login-root').show();
|
||||
});
|
||||
//switch main video
|
||||
$('#main-video').on('click', () => {
|
||||
let mainVideo = $('.video-box').first();
|
||||
if ($('#main-video').is(mainVideo)) {
|
||||
return;
|
||||
}
|
||||
//释放main-video grid-area
|
||||
mainVideo.css('grid-area', 'auto/auto/auto/auto');
|
||||
exchangeView($('#main-video'), mainVideo);
|
||||
//将video-grid中第一个div设为main-video
|
||||
$('.video-box')
|
||||
.first()
|
||||
.css('grid-area', '1/1/3/4');
|
||||
//chromeM71以下会自动暂停,手动唤醒
|
||||
if (getBroswer().broswer == 'Chrome' && getBroswer().version < '72') {
|
||||
rtc.resumeStreams();
|
||||
}
|
||||
});
|
||||
|
||||
//chrome60以下不支持popover,防止error
|
||||
if (getBroswer().broswer == 'Chrome' && getBroswer().version < '60') return;
|
||||
//开启popover
|
||||
$(function() {
|
||||
$('[data-toggle="popover"]').popover();
|
||||
});
|
||||
$('#camera').popover({
|
||||
html: true,
|
||||
content: () => {
|
||||
return $('#camera-option').html();
|
||||
}
|
||||
});
|
||||
$('#microphone').popover({
|
||||
html: true,
|
||||
content: () => {
|
||||
return $('#mic-option').html();
|
||||
}
|
||||
});
|
||||
|
||||
$('#camera').on('click', () => {
|
||||
$('#microphone').popover('hide');
|
||||
$('.popover-body')
|
||||
.find('div')
|
||||
.attr('onclick', 'setCameraId(this)');
|
||||
});
|
||||
|
||||
$('#microphone').on('click', () => {
|
||||
$('#camera').popover('hide');
|
||||
$('.popover-body')
|
||||
.find('div')
|
||||
.attr('onclick', 'setMicId(this)');
|
||||
});
|
||||
|
||||
//点击body关闭popover
|
||||
$('body').click(() => {
|
||||
$('#camera').popover('hide');
|
||||
$('#microphone').popover('hide');
|
||||
});
|
||||
|
||||
//popover事件
|
||||
$('#camera').on('show.bs.popover', () => {
|
||||
$('#camera').attr('src', './img/camera-on.png');
|
||||
});
|
||||
$('#camera').on('hide.bs.popover', () => {
|
||||
$('#camera').attr('src', './img/camera.png');
|
||||
});
|
||||
|
||||
$('#microphone').on('show.bs.popover', () => {
|
||||
$('#microphone').attr('src', './img/mic-on.png');
|
||||
});
|
||||
$('#microphone').on('hide.bs.popover', () => {
|
||||
$('#microphone').attr('src', './img/mic.png');
|
||||
});
|
||||
}
|
||||
|
||||
function setCameraId(thisDiv) {
|
||||
cameraId = $(thisDiv).attr('id');
|
||||
console.log('setCameraId: ' + cameraId);
|
||||
}
|
||||
|
||||
function setMicId(thisDiv) {
|
||||
micId = $(thisDiv).attr('id');
|
||||
console.log('setMicId: ' + micId);
|
||||
}
|
||||
|
||||
function addVideoView(id, isLocal = false) {
|
||||
let div = $('<div/>', {
|
||||
id: id,
|
||||
class: 'video-box',
|
||||
style: 'justify-content: center'
|
||||
});
|
||||
div.appendTo('#video-grid');
|
||||
//设置监听
|
||||
div.click(() => {
|
||||
let mainVideo = $('.video-box').first();
|
||||
if (div.is(mainVideo)) {
|
||||
return;
|
||||
}
|
||||
//释放main-video grid-area
|
||||
mainVideo.css('grid-area', 'auto/auto/auto/auto');
|
||||
exchangeView(div, mainVideo);
|
||||
//将video-grid中第一个div设为main-video
|
||||
$('.video-box')
|
||||
.first()
|
||||
.css('grid-area', '1/1/3/4');
|
||||
//chromeM71以下会自动暂停,手动唤醒
|
||||
if (getBroswer().broswer == 'Chrome' && getBroswer().version < '72') {
|
||||
rtc.resumeStreams();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addMemberView(id) {
|
||||
let memberElm = $('#member-me').clone();
|
||||
memberElm.attr('id', id);
|
||||
memberElm.find('div.member-id').html(id);
|
||||
memberElm.css('display', 'flex');
|
||||
memberElm.appendTo($('#member-list'));
|
||||
}
|
||||
|
||||
function removeView(id) {
|
||||
if ($('#' + id)[0]) {
|
||||
$('#' + id).remove();
|
||||
//将video-grid中第一个div设为main-video
|
||||
$('.video-box')
|
||||
.first()
|
||||
.css('grid-area', '1/1/3/4');
|
||||
}
|
||||
}
|
||||
|
||||
function exchangeView(a, b) {
|
||||
var $div1 = $(a);
|
||||
var $div3 = $(b);
|
||||
var $temobj1 = $('<div></div>');
|
||||
var $temobj2 = $('<div></div>');
|
||||
$temobj1.insertBefore($div1);
|
||||
$temobj2.insertBefore($div3);
|
||||
$div1.insertAfter($temobj2);
|
||||
$div3.insertAfter($temobj1);
|
||||
$temobj1.remove();
|
||||
$temobj2.remove();
|
||||
}
|
||||
|
||||
function isPC() {
|
||||
var userAgentInfo = navigator.userAgent;
|
||||
var Agents = new Array('Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod');
|
||||
var flag = true;
|
||||
for (var v = 0; v < Agents.length; v++) {
|
||||
if (userAgentInfo.indexOf(Agents[v]) > 0) {
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
function getCameraId() {
|
||||
console.log('selected cameraId: ' + cameraId);
|
||||
return cameraId;
|
||||
}
|
||||
|
||||
function getMicrophoneId() {
|
||||
console.log('selected microphoneId: ' + micId);
|
||||
return micId;
|
||||
}
|
||||
|
||||
function throttle(func, delay) {
|
||||
var timer = null;
|
||||
var startTime = Date.now();
|
||||
return function() {
|
||||
var curTime = Date.now();
|
||||
var remaining = delay - (curTime - startTime);
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
clearTimeout(timer);
|
||||
if (remaining <= 0) {
|
||||
func.apply(context, args);
|
||||
startTime = Date.now();
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
console.log('duplicate click');
|
||||
}, remaining);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resetView() {
|
||||
isCamOn = true;
|
||||
isMicOn = true;
|
||||
isScreenOn = false;
|
||||
isJoined = true;
|
||||
$('#main-video-btns').hide();
|
||||
$('#video-btn').attr('src', './img/big-camera-on.png');
|
||||
$('#mic-btn').attr('src', './img/big-mic-on.png');
|
||||
$('#screen-btn').attr('src', './img/screen-off.png');
|
||||
$('#member-me')
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-on.png');
|
||||
$('#member-me')
|
||||
.find('.member-audio-btn')
|
||||
.attr('src', 'img/mic-on.png');
|
||||
$('.mask').hide();
|
||||
//清空member-list
|
||||
if ($('#member-list')) {
|
||||
$('#member-list')
|
||||
.find('.member')
|
||||
.each((index, element) => {
|
||||
if (
|
||||
$(element)
|
||||
.parent()
|
||||
.attr('id') != 'member-me'
|
||||
) {
|
||||
$(element)
|
||||
.parent()
|
||||
.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getBroswer() {
|
||||
var sys = {};
|
||||
var ua = navigator.userAgent.toLowerCase();
|
||||
var s;
|
||||
(s = ua.match(/edge\/([\d.]+)/))
|
||||
? (sys.edge = s[1])
|
||||
: (s = ua.match(/rv:([\d.]+)\) like gecko/))
|
||||
? (sys.ie = s[1])
|
||||
: (s = ua.match(/msie ([\d.]+)/))
|
||||
? (sys.ie = s[1])
|
||||
: (s = ua.match(/firefox\/([\d.]+)/))
|
||||
? (sys.firefox = s[1])
|
||||
: (s = ua.match(/chrome\/([\d.]+)/))
|
||||
? (sys.chrome = s[1])
|
||||
: (s = ua.match(/opera.([\d.]+)/))
|
||||
? (sys.opera = s[1])
|
||||
: (s = ua.match(/version\/([\d.]+).*safari/))
|
||||
? (sys.safari = s[1])
|
||||
: 0;
|
||||
|
||||
if (sys.edge) return { broswer: 'Edge', version: sys.edge };
|
||||
if (sys.ie) return { broswer: 'IE', version: sys.ie };
|
||||
if (sys.firefox) return { broswer: 'Firefox', version: sys.firefox };
|
||||
if (sys.chrome) return { broswer: 'Chrome', version: sys.chrome };
|
||||
if (sys.opera) return { broswer: 'Opera', version: sys.opera };
|
||||
if (sys.safari) return { broswer: 'Safari', version: sys.safari };
|
||||
|
||||
return { broswer: '', version: '0' };
|
||||
}
|
||||
|
||||
function isHidden() {
|
||||
var hidden, visibilityChange;
|
||||
if (typeof document.hidden !== 'undefined') {
|
||||
hidden = 'hidden';
|
||||
visibilityChange = 'visibilitychange';
|
||||
} else if (typeof document.msHidden !== 'undefined') {
|
||||
hidden = 'msHidden';
|
||||
visibilityChange = 'msvisibilitychange';
|
||||
} else if (typeof document.webkitHidden !== 'undefined') {
|
||||
hidden = 'webkitHidden';
|
||||
visibilityChange = 'webkitvisibilitychange';
|
||||
}
|
||||
return document[hidden];
|
||||
}
|
||||
|
||||
function getIPAddress() {
|
||||
return new Promise(resolve => {
|
||||
window.RTCPeerConnection =
|
||||
window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; //compatibility for firefox and chrome
|
||||
let pc = new RTCPeerConnection({ iceServers: [] });
|
||||
let noop = function() {};
|
||||
let IPAddress = '';
|
||||
let ipRegex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/;
|
||||
pc.createDataChannel(''); //create a bogus data channel
|
||||
pc.createOffer(pc.setLocalDescription.bind(pc), noop); // create offer and set local description
|
||||
//listen for candidate events
|
||||
pc.onicecandidate = function(ice) {
|
||||
if (
|
||||
!ice ||
|
||||
!ice.candidate ||
|
||||
!ice.candidate.candidate ||
|
||||
!ipRegex.exec(ice.candidate.candidate)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
IPAddress = ipRegex.exec(ice.candidate.candidate)[1];
|
||||
pc.onicecandidate = noop;
|
||||
resolve(IPAddress);
|
||||
};
|
||||
});
|
||||
}
|
||||
let isMobile = {
|
||||
Android: function() {
|
||||
return navigator.userAgent.match(/Android/i);
|
||||
},
|
||||
BlackBerry: function() {
|
||||
return navigator.userAgent.match(/BlackBerry|BB10/i);
|
||||
},
|
||||
iOS: function() {
|
||||
return navigator.userAgent.match(/iPhone|iPad|iPod/i);
|
||||
},
|
||||
Opera: function() {
|
||||
return navigator.userAgent.match(/Opera Mini/i);
|
||||
},
|
||||
Windows: function() {
|
||||
return navigator.userAgent.match(/IEMobile/i);
|
||||
},
|
||||
any: function() {
|
||||
return (
|
||||
isMobile.Android() ||
|
||||
isMobile.BlackBerry() ||
|
||||
isMobile.iOS() ||
|
||||
isMobile.Opera() ||
|
||||
isMobile.Windows()
|
||||
);
|
||||
},
|
||||
getOsName: function() {
|
||||
var osName = 'Unknown OS';
|
||||
if (isMobile.Android()) {
|
||||
osName = 'Android';
|
||||
}
|
||||
if (isMobile.BlackBerry()) {
|
||||
osName = 'BlackBerry';
|
||||
}
|
||||
if (isMobile.iOS()) {
|
||||
osName = 'iOS';
|
||||
}
|
||||
if (isMobile.Opera()) {
|
||||
osName = 'Opera Mini';
|
||||
}
|
||||
if (isMobile.Windows()) {
|
||||
osName = 'Windows';
|
||||
}
|
||||
return osName;
|
||||
}
|
||||
};
|
||||
function detectDesktopOS() {
|
||||
var unknown = '-';
|
||||
var nVer = navigator.appVersion;
|
||||
var nAgt = navigator.userAgent;
|
||||
var os = unknown;
|
||||
var clientStrings = [
|
||||
{
|
||||
s: 'Chrome OS',
|
||||
r: /CrOS/
|
||||
},
|
||||
{
|
||||
s: 'Windows 10',
|
||||
r: /(Windows 10.0|Windows NT 10.0)/
|
||||
},
|
||||
{
|
||||
s: 'Windows 8.1',
|
||||
r: /(Windows 8.1|Windows NT 6.3)/
|
||||
},
|
||||
{
|
||||
s: 'Windows 8',
|
||||
r: /(Windows 8|Windows NT 6.2)/
|
||||
},
|
||||
{
|
||||
s: 'Windows 7',
|
||||
r: /(Windows 7|Windows NT 6.1)/
|
||||
},
|
||||
{
|
||||
s: 'Windows Vista',
|
||||
r: /Windows NT 6.0/
|
||||
},
|
||||
{
|
||||
s: 'Windows Server 2003',
|
||||
r: /Windows NT 5.2/
|
||||
},
|
||||
{
|
||||
s: 'Windows XP',
|
||||
r: /(Windows NT 5.1|Windows XP)/
|
||||
},
|
||||
{
|
||||
s: 'Windows 2000',
|
||||
r: /(Windows NT 5.0|Windows 2000)/
|
||||
},
|
||||
{
|
||||
s: 'Windows ME',
|
||||
r: /(Win 9x 4.90|Windows ME)/
|
||||
},
|
||||
{
|
||||
s: 'Windows 98',
|
||||
r: /(Windows 98|Win98)/
|
||||
},
|
||||
{
|
||||
s: 'Windows 95',
|
||||
r: /(Windows 95|Win95|Windows_95)/
|
||||
},
|
||||
{
|
||||
s: 'Windows NT 4.0',
|
||||
r: /(Windows NT 4.0|WinNT4.0|WinNT|Windows NT)/
|
||||
},
|
||||
{
|
||||
s: 'Windows CE',
|
||||
r: /Windows CE/
|
||||
},
|
||||
{
|
||||
s: 'Windows 3.11',
|
||||
r: /Win16/
|
||||
},
|
||||
{
|
||||
s: 'Android',
|
||||
r: /Android/
|
||||
},
|
||||
{
|
||||
s: 'Open BSD',
|
||||
r: /OpenBSD/
|
||||
},
|
||||
{
|
||||
s: 'Sun OS',
|
||||
r: /SunOS/
|
||||
},
|
||||
{
|
||||
s: 'Linux',
|
||||
r: /(Linux|X11)/
|
||||
},
|
||||
{
|
||||
s: 'iOS',
|
||||
r: /(iPhone|iPad|iPod)/
|
||||
},
|
||||
{
|
||||
s: 'Mac OS X',
|
||||
r: /Mac OS X/
|
||||
},
|
||||
{
|
||||
s: 'Mac OS',
|
||||
r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/
|
||||
},
|
||||
{
|
||||
s: 'QNX',
|
||||
r: /QNX/
|
||||
},
|
||||
{
|
||||
s: 'UNIX',
|
||||
r: /UNIX/
|
||||
},
|
||||
{
|
||||
s: 'BeOS',
|
||||
r: /BeOS/
|
||||
},
|
||||
{
|
||||
s: 'OS/2',
|
||||
r: /OS\/2/
|
||||
},
|
||||
{
|
||||
s: 'Search Bot',
|
||||
r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/
|
||||
}
|
||||
];
|
||||
for (var i = 0, cs; (cs = clientStrings[i]); i++) {
|
||||
if (cs.r.test(nAgt)) {
|
||||
os = cs.s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
var osVersion = unknown;
|
||||
if (/Windows/.test(os)) {
|
||||
if (/Windows (.*)/.test(os)) {
|
||||
osVersion = /Windows (.*)/.exec(os)[1];
|
||||
}
|
||||
os = 'Windows';
|
||||
}
|
||||
switch (os) {
|
||||
case 'Mac OS X':
|
||||
if (/Mac OS X (10[/._\d]+)/.test(nAgt)) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
osVersion = /Mac OS X (10[\.\_\d]+)/.exec(nAgt)[1];
|
||||
}
|
||||
break;
|
||||
case 'Android':
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
if (/Android ([\.\_\d]+)/.test(nAgt)) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
osVersion = /Android ([\.\_\d]+)/.exec(nAgt)[1];
|
||||
}
|
||||
break;
|
||||
case 'iOS':
|
||||
if (/OS (\d+)_(\d+)_?(\d+)?/.test(nAgt)) {
|
||||
osVersion = /OS (\d+)_(\d+)_?(\d+)?/.exec(nVer);
|
||||
osVersion = osVersion[1] + '.' + osVersion[2] + '.' + (osVersion[3] | 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {
|
||||
osName: os + osVersion
|
||||
};
|
||||
}
|
||||
function getOS() {
|
||||
if (isMobile.any()) {
|
||||
return isMobile.getOsName();
|
||||
} else {
|
||||
return detectDesktopOS();
|
||||
}
|
||||
}
|
63
public/js/debug/GenerateTestUserSig.js
Normal file
@ -0,0 +1,63 @@
|
||||
/* eslint-disable require-jsdoc */
|
||||
/*
|
||||
* Module: GenerateTestUserSig
|
||||
*
|
||||
* Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
|
||||
* 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
|
||||
*
|
||||
* Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
|
||||
*
|
||||
* 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
|
||||
* 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
|
||||
* 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
|
||||
*
|
||||
* 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
|
||||
* 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
|
||||
*
|
||||
* Reference:https://cloud.tencent.com/document/product/647/17275#Server
|
||||
*/
|
||||
function genTestUserSig(userID) {
|
||||
/**
|
||||
* 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
|
||||
*
|
||||
* 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId,
|
||||
* 它是腾讯云用于区分客户的唯一标识。
|
||||
*/
|
||||
const SDKAPPID = 1400435767;
|
||||
|
||||
/**
|
||||
* 签名过期时间,建议不要设置的过短
|
||||
* <p>
|
||||
* 时间单位:秒
|
||||
* 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
|
||||
*/
|
||||
const EXPIRETIME = 604800;
|
||||
|
||||
/**
|
||||
* 计算签名用的加密密钥,获取步骤如下:
|
||||
*
|
||||
* step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个,
|
||||
* step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
|
||||
* step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
|
||||
*
|
||||
* 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
|
||||
* 文档:https://cloud.tencent.com/document/product/647/17275#Server
|
||||
*/
|
||||
const SECRETKEY = 'dzrUpsgeMo0ygiSmqeDVqxnLbdT3Lbbh';
|
||||
|
||||
// a soft reminder to guide developer to configure sdkAppId/secretKey
|
||||
if (SDKAPPID === '' || SECRETKEY === '') {
|
||||
alert(
|
||||
'请先配置好您的账号信息: SDKAPPID 及 SECRETKEY ' +
|
||||
'\r\n\r\nPlease configure your SDKAPPID/SECRETKEY in js/debug/GenerateTestUserSig.js'
|
||||
);
|
||||
}
|
||||
const generator = new LibGenerateTestUserSig(SDKAPPID, SECRETKEY, EXPIRETIME);
|
||||
const userSig = window.istow ? "eJwtzdEKgjAUBuB32XXYdJubQhcV2IWBkGYEgohOO1SypoYQvXtLvTzff85-Pig5xtZbauQjx8JoNc1QybaHGibuboWW*dBJnRNMMcYeX9a66l4oBRXybeOUMO7yOZGjAi2NM8YcczJrD8*-uZhhYRPBlhZozJfW4*f9y7EPafmgTXjdnYQagjIRY7Ye4qim20sNaRsGEWk26PsDMPg1Xg__" : "eJwtzE8LgkAQBfDvstdCJt3RTeigl6gMw-7oLUK3GCNZ1Foj*u5t6vH93sz7sEO0t16yZj6zLWDTPlMhq5au1POzkfXZAQ4Ac288aIr7RSkqmD8zzh30XG9oZKeolsYR0TYvg7b0*JsLCAIcjuMK3cx*LmyV5GFXBWGZ6m4VVaejzjax3K3fehsTpk2QJKVeTsSCfX94djN6";
|
||||
console.log(userSig)
|
||||
window.istow = true
|
||||
return {
|
||||
sdkAppId: SDKAPPID,
|
||||
userSig:userSig
|
||||
};
|
||||
}
|
838
public/js/device-testing.js
Normal file
@ -0,0 +1,838 @@
|
||||
/**
|
||||
* 设备检测demo
|
||||
*/
|
||||
/* global $ TRTC presetting getOS getBroswer cameraId micId */
|
||||
|
||||
// 用于记录检测结果,生成检测报告
|
||||
let hasCameraDevice = false,
|
||||
hasMicAndVoiceDevice = false,
|
||||
hasCameraConnect,
|
||||
hasVoiceConnect,
|
||||
hasMicConnect,
|
||||
hasNetworkConnect;
|
||||
let cameraTestingResult = {};
|
||||
let voiceTestingResult = {};
|
||||
let micTestingResult = {};
|
||||
let networkTestingResult = {};
|
||||
|
||||
// 记录检测步骤,用于关闭时清空弹窗
|
||||
let completedTestingPageIdList = [];
|
||||
let curTestingPageId = '';
|
||||
let localStream = null;
|
||||
let client = null;
|
||||
let timeout = null;
|
||||
// 监听到network-quality事件的次数
|
||||
let networkQualityNum = 0;
|
||||
|
||||
const deviceFailAttention =
|
||||
'1. 若浏览器弹出提示,请选择“允许”<br>' +
|
||||
'2. 若杀毒软件弹出提示,请选择“允许”<br>' +
|
||||
'3. 检查浏览器设置,允许网页访问摄像头及麦克风<br>' +
|
||||
'4. 检查摄像头/麦克风是否正确连接并开启<br>' +
|
||||
'5. 尝试重新连接摄像头/麦克风<br>' +
|
||||
'6. 尝试重启电脑后重新检测';
|
||||
const networkFailAttention =
|
||||
'1. 请检查设备是否联网<br>' + '2. 请刷新网页后再次检测<br>' + '3. 请尝试更换网络后再次检测';
|
||||
|
||||
// 网络参数对照表
|
||||
const NETWORK_QUALITY = {
|
||||
'0': '未知',
|
||||
'1': '极佳',
|
||||
'2': '较好',
|
||||
'3': '一般',
|
||||
'4': '差',
|
||||
'5': '极差',
|
||||
'6': '断开'
|
||||
};
|
||||
|
||||
// 设备检测tab页签对应的执行方法
|
||||
const pageCallbackConfig = {
|
||||
'camera-testing-body': 'startCameraTesting',
|
||||
'voice-testing-body': 'startVoiceTesting',
|
||||
'mic-testing-body': 'startMicTesting',
|
||||
'network-testing-body': 'startNetworkTesting'
|
||||
};
|
||||
|
||||
// 判断是否为safari浏览器
|
||||
let isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
|
||||
hideVoiceForSafari();
|
||||
/**
|
||||
* safari浏览器中隐藏扬声器相关检测
|
||||
*/
|
||||
function hideVoiceForSafari() {
|
||||
if (!isSafari) return;
|
||||
$('#connect-voice').hide();
|
||||
$('#device-voice').hide();
|
||||
$('#voice-testing').hide();
|
||||
$('#voice-report').hide();
|
||||
$('#device-mic').addClass('safari');
|
||||
$('#device-network').addClass('safari');
|
||||
$('#mic-testing').addClass('safari');
|
||||
$('#network-testing').addClass('safari');
|
||||
}
|
||||
|
||||
// 是否是本地路径打开
|
||||
let isFilePath = location.href.indexOf('file://') > -1;
|
||||
|
||||
/**
|
||||
* 设备检测初始化
|
||||
*/
|
||||
async function deviceTestingInit() {
|
||||
// 点击【设备检测】文字, 点击 【重新连接】按钮
|
||||
$('#device-testing-btn, #connect-again-btn').on('click', () => {
|
||||
startDeviceConnect();
|
||||
});
|
||||
// 连接设备错误icon
|
||||
$('#connect-attention-icon').on('mouseover', () => {
|
||||
$('#connect-attention-info').show();
|
||||
});
|
||||
// 连接设备错误icon
|
||||
$('#connect-attention-icon').on('mouseout', () => {
|
||||
$('#connect-attention-info').hide();
|
||||
});
|
||||
// 【开始检测】开始设备检测按钮
|
||||
$('#start-test-btn').on('click', function() {
|
||||
if ($(this).hasClass('start-gray')) return;
|
||||
$('#device-testing-prepare').hide();
|
||||
$('#device-testing').show();
|
||||
startCameraTesting();
|
||||
});
|
||||
// 摄像头检测失败/成功
|
||||
$('#camera-fail, #camera-success').on('click', function() {
|
||||
cameraTestingResult.statusResult = $(this).attr('id') === 'camera-success';
|
||||
$('#camera-testing-body').hide();
|
||||
localStream.close();
|
||||
// safari浏览器跳过扬声器检测
|
||||
isSafari ? startMicTesting() : startVoiceTesting();
|
||||
});
|
||||
// 播放器检测失败/成功
|
||||
$('#voice-fail, #voice-success').on('click', function() {
|
||||
voiceTestingResult.statusResult = $(this).attr('id') === 'voice-success';
|
||||
$('#voice-testing-body').hide();
|
||||
let audioPlayer = document.querySelector('#audio-player');
|
||||
if (!audioPlayer.paused) {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
startMicTesting();
|
||||
});
|
||||
// 麦克风测试失败/成功
|
||||
$('#mic-fail, #mic-success').on('click', function() {
|
||||
micTestingResult.statusResult = $(this).attr('id') === 'mic-success';
|
||||
$('#mic-testing-body').hide();
|
||||
localStream.close();
|
||||
startNetworkTesting();
|
||||
});
|
||||
// 点击【查看检测报告】按钮
|
||||
$('#testing-report-btn').on('click', () => {
|
||||
showTestingReport();
|
||||
localStream.close();
|
||||
client && client.leave();
|
||||
client && client.off('network-quality');
|
||||
});
|
||||
// 点击【重新测试】按钮
|
||||
$('#testing-again').on('click', () => {
|
||||
$('#device-testing-report').hide();
|
||||
startDeviceConnect();
|
||||
completedTestingPageIdList = [];
|
||||
});
|
||||
// 点击【测试完成】按钮 / 点击关闭图标
|
||||
$('#testing-finish, #device-testing-close-btn').on('click', () => {
|
||||
finishDeviceTesting();
|
||||
});
|
||||
// 测试tab页切换
|
||||
$('#camera-testing, #voice-testing, #mic-testing, #network-testing').on('click', function() {
|
||||
let targetPageId = $(this).attr('id') + '-body';
|
||||
if (
|
||||
targetPageId !== curTestingPageId &&
|
||||
completedTestingPageIdList.indexOf(targetPageId) > -1
|
||||
) {
|
||||
$(`#${curTestingPageId}`).hide();
|
||||
localStream && localStream.close();
|
||||
client && client.leave();
|
||||
client && client.off('network-quality');
|
||||
// 停止播放器的音乐
|
||||
let audioPlayer = document.querySelector('#audio-player');
|
||||
if (!audioPlayer.paused) {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
// 展示要切换的设备检测tab页面
|
||||
$(`#${targetPageId}`).show();
|
||||
window[pageCallbackConfig[targetPageId]] && window[pageCallbackConfig[targetPageId]]();
|
||||
}
|
||||
});
|
||||
// 摄像头设备切换
|
||||
$('#camera-select').change(async function() {
|
||||
let newCameraId = $(this)
|
||||
.children('option:selected')
|
||||
.val();
|
||||
localStorage.setItem('txy_webRTC_cameraId', newCameraId);
|
||||
cameraTestingResult.device = {
|
||||
label: $(this)
|
||||
.children('option:selected')
|
||||
.text(),
|
||||
deviceId: $(this)
|
||||
.children('option:selected')
|
||||
.val(),
|
||||
kind: 'videoinput'
|
||||
};
|
||||
await localStream.switchDevice('video', newCameraId);
|
||||
});
|
||||
// 扬声器设备切换
|
||||
$('#voice-select').change(async function() {
|
||||
let newVoiceId = $(this)
|
||||
.children('option:selected')
|
||||
.val();
|
||||
localStorage.setItem('txy_webRTC_voiceId', newVoiceId);
|
||||
voiceTestingResult.device = {
|
||||
label: $(this)
|
||||
.children('option:selected')
|
||||
.text(),
|
||||
deviceId: $(this)
|
||||
.children('option:selected')
|
||||
.val(),
|
||||
kind: 'audiooutput'
|
||||
};
|
||||
|
||||
let audioPlayer = document.querySelector('#audio-player');
|
||||
await audioPlayer.setSinkId(newVoiceId);
|
||||
});
|
||||
// 麦克风设备切换
|
||||
$('#mic-select').change(async function() {
|
||||
let newMicID = $(this)
|
||||
.children('option:selected')
|
||||
.val();
|
||||
localStorage.setItem('txy_webRTC_micId', newMicID);
|
||||
micTestingResult.device = {
|
||||
label: $(this)
|
||||
.children('option:selected')
|
||||
.text(),
|
||||
deviceId: $(this)
|
||||
.children('option:selected')
|
||||
.val(),
|
||||
kind: 'audioinput'
|
||||
};
|
||||
await localStream.switchDevice('audio', newMicID);
|
||||
});
|
||||
|
||||
$('body').on('click', function() {
|
||||
$('#device-connect-list').hide();
|
||||
});
|
||||
|
||||
// 获取设备信息
|
||||
await getDevicesInfo();
|
||||
// 初始化设备弹窗信息
|
||||
deviceDialogInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备信息及网络连接信息
|
||||
*/
|
||||
async function getDevicesInfo() {
|
||||
let micList = await TRTC.getMicrophones();
|
||||
let voiceList = await TRTC.getSpeakers();
|
||||
let cameraList = await TRTC.getCameras();
|
||||
let index = isFilePath ? 'label' : 'deviceId';
|
||||
if (cameraList.length > 0) {
|
||||
hasCameraDevice = true;
|
||||
}
|
||||
if (micList.length > 0) {
|
||||
hasMicAndVoiceDevice = true;
|
||||
}
|
||||
cameraList.forEach(camera => {
|
||||
if (camera[index].length > 0) {
|
||||
hasCameraConnect = true;
|
||||
}
|
||||
});
|
||||
micList.forEach(mic => {
|
||||
if (mic[index].length > 0) {
|
||||
hasMicConnect = true;
|
||||
}
|
||||
});
|
||||
if (isSafari) {
|
||||
hasVoiceConnect = true;
|
||||
} else {
|
||||
voiceList.forEach(voice => {
|
||||
if (voice[index].length > 0) {
|
||||
hasVoiceConnect = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
hasNetworkConnect = !!navigator.onLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否展示弹窗
|
||||
*/
|
||||
function deviceDialogInit() {
|
||||
if (!localStorage.getItem('txy_device_testing')) {
|
||||
localStorage.setItem('txy_device_testing', Date.now());
|
||||
startDeviceConnect();
|
||||
} else {
|
||||
// 在首页展示设备连接结果
|
||||
let showDeviceStatus = function() {
|
||||
$('#device-connect-list').show();
|
||||
timeout = setTimeout(() => {
|
||||
$('#device-connect-list').hide();
|
||||
}, 3000);
|
||||
$('#connect-camera').css('color', `${hasCameraConnect ? 'green' : 'red'}`);
|
||||
$('#connect-voice').css('color', `${hasVoiceConnect ? 'green' : 'red'}`);
|
||||
$('#connect-mic').css('color', `${hasMicConnect ? 'green' : 'red'}`);
|
||||
$('#connect-network').css('color', `${hasNetworkConnect ? 'green' : 'red'}`);
|
||||
if (!(hasCameraConnect && hasVoiceConnect && hasMicConnect && hasNetworkConnect)) {
|
||||
$('#device-testing-btn').css('color', 'red');
|
||||
} else {
|
||||
$('#device-testing-btn').css('color', 'green');
|
||||
}
|
||||
};
|
||||
showDeviceStatus();
|
||||
|
||||
if (!(hasCameraConnect && hasVoiceConnect && hasMicConnect)) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: hasCameraDevice, audio: hasMicAndVoiceDevice })
|
||||
.then(async () => {
|
||||
// 重新获取设备信息
|
||||
await getDevicesInfo();
|
||||
// 更新首页popover的option list
|
||||
getDevicesList();
|
||||
// 展示连接结果
|
||||
showDeviceStatus();
|
||||
})
|
||||
.catch(err => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗-设备连接检查
|
||||
*/
|
||||
function startDeviceConnect() {
|
||||
// 显示设备检测弹窗
|
||||
$('#device-testing-root').show();
|
||||
// 设备检测弹窗-设备连接页
|
||||
$('#device-testing-prepare').show();
|
||||
|
||||
curTestingPageId = 'device-testing-prepare';
|
||||
initTestingTabTitle();
|
||||
|
||||
// 在设备检测弹窗显示设备连接信息
|
||||
let showDeviceConnectInfo = function() {
|
||||
if (!(hasCameraConnect && hasVoiceConnect && hasMicConnect && hasNetworkConnect)) {
|
||||
$('#device-testing-btn').css('color', 'red');
|
||||
} else {
|
||||
$('#device-testing-btn').css('color', 'green');
|
||||
}
|
||||
// 隐藏设备连接失败提示
|
||||
$('#connect-attention-container').hide();
|
||||
|
||||
// 设备连接中
|
||||
$('#device-loading').show();
|
||||
$('#connect-info')
|
||||
.text('设备正在连接中,请稍等')
|
||||
.css('color', '#cccccc');
|
||||
$('#device-camera, #device-voice, #device-mic, #device-network').removeClass(
|
||||
'connect-success connect-fail'
|
||||
);
|
||||
$('#connect-again-btn').hide();
|
||||
$('#start-test-btn')
|
||||
.addClass('start-gray')
|
||||
.show();
|
||||
|
||||
// 设备连接结束,展示连接结果
|
||||
setTimeout(() => {
|
||||
$('#device-loading').hide();
|
||||
$('#device-camera')
|
||||
.removeClass('connect-success connect-fail')
|
||||
.addClass(`${hasCameraConnect ? 'connect-success' : 'connect-fail'}`);
|
||||
$('#device-voice')
|
||||
.removeClass('connect-success connect-fail')
|
||||
.addClass(`${hasVoiceConnect ? 'connect-success' : 'connect-fail'}`);
|
||||
$('#device-mic')
|
||||
.removeClass('connect-success connect-fail')
|
||||
.addClass(`${hasMicConnect ? 'connect-success' : 'connect-fail'}`);
|
||||
$('#device-network')
|
||||
.removeClass('connect-success connect-fail')
|
||||
.addClass(`${hasNetworkConnect ? 'connect-success' : 'connect-fail'}`);
|
||||
|
||||
if (!(hasCameraConnect && hasVoiceConnect && hasMicConnect)) {
|
||||
let connectInfo = hasNetworkConnect
|
||||
? '设备连接失败,请允许网页访问摄像头及麦克风'
|
||||
: '设备及网络连接失败,请允许网页访问摄像头及麦克风并检查网络连接';
|
||||
$('#connect-info')
|
||||
.text(connectInfo)
|
||||
.css('color', 'red');
|
||||
// 显示设备连接失败引导
|
||||
$('#connect-attention-container').show();
|
||||
$('#connect-attention-info').html(deviceFailAttention);
|
||||
// 切换按钮状态
|
||||
$('#start-test-btn').hide();
|
||||
$('#connect-again-btn').show();
|
||||
}
|
||||
if (hasCameraConnect && hasVoiceConnect && hasMicConnect && !hasNetworkConnect) {
|
||||
$('#connect-info')
|
||||
.text('网络连接失败,请检查网络连接')
|
||||
.css('color', 'red');
|
||||
// 显示网络连接失败引导
|
||||
$('#connect-attention-container').show();
|
||||
$('#connect-attention-info').html(networkFailAttention);
|
||||
// 切换按钮状态
|
||||
$('#start-test-btn').hide();
|
||||
$('#connect-again-btn').show();
|
||||
}
|
||||
if (hasCameraConnect && hasVoiceConnect && hasMicConnect && hasNetworkConnect) {
|
||||
$('#connect-info')
|
||||
.text('设备及网络连接成功,请开始设备检测')
|
||||
.css('color', '#32CD32');
|
||||
$('#connect-again-btn').hide();
|
||||
$('#start-test-btn')
|
||||
.removeClass('start-gray')
|
||||
.show();
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
showDeviceConnectInfo();
|
||||
|
||||
// 如果有设备未连接,唤起请求弹窗
|
||||
if (!(hasCameraConnect && hasVoiceConnect && hasMicConnect)) {
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({ video: hasCameraDevice, audio: hasMicAndVoiceDevice })
|
||||
.then(async () => {
|
||||
// 重新获取设备信息
|
||||
await getDevicesInfo();
|
||||
// 更新首页popover的option list
|
||||
getDevicesList();
|
||||
// 显示设备连接信息
|
||||
showDeviceConnectInfo();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('err', err.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新首页popover的option list
|
||||
*/
|
||||
function getDevicesList() {
|
||||
// populate camera options
|
||||
TRTC.getCameras().then(devices => {
|
||||
$('#camera-option').empty();
|
||||
devices.forEach(device => {
|
||||
if (!cameraId) {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
cameraId = device.deviceId;
|
||||
}
|
||||
let div = $('<div></div>');
|
||||
div.attr('id', device.deviceId);
|
||||
div.html(device.label);
|
||||
div.appendTo('#camera-option');
|
||||
});
|
||||
});
|
||||
|
||||
// populate microphone options
|
||||
TRTC.getMicrophones().then(devices => {
|
||||
$('#mic-option').empty();
|
||||
devices.forEach(device => {
|
||||
if (!micId) {
|
||||
// eslint-disable-next-line no-global-assign
|
||||
micId = device.deviceId;
|
||||
}
|
||||
let div = $('<div></div>');
|
||||
div.attr('id', device.deviceId);
|
||||
div.html(device.label);
|
||||
div.appendTo('#mic-option');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 摄像头检测页-检测展示摄像头设备选择列表
|
||||
*/
|
||||
async function updateCameraDeviceList() {
|
||||
let cameraDevices = await TRTC.getCameras();
|
||||
cameraDevices.filter(camera => camera.deviceId !== 'default');
|
||||
$('#camera-select').empty();
|
||||
cameraDevices.forEach(camera => {
|
||||
let option = $('<option></option>');
|
||||
option.attr('value', camera.deviceId);
|
||||
option.html(camera.label);
|
||||
option.appendTo('#camera-select');
|
||||
});
|
||||
|
||||
// 如果有用户设备选择缓存,优先使用缓存的deviceId
|
||||
let cacheCameraDevice = cameraDevices.filter(
|
||||
camera => camera.deviceId === localStorage.getItem('txy_webRTC_cameraId')
|
||||
);
|
||||
if (cacheCameraDevice.length > 0) {
|
||||
$('#camera-select').val(localStorage.getItem('txy_webRTC_cameraId'));
|
||||
cameraTestingResult.device = cacheCameraDevice[0];
|
||||
} else {
|
||||
$('#camera-select').val(cameraDevices[0].deviceId);
|
||||
cameraTestingResult.device = cameraDevices[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 摄像头设备测试
|
||||
*/
|
||||
async function startCameraTesting() {
|
||||
$('#camera-testing-body').show();
|
||||
curTestingPageId = 'camera-testing-body';
|
||||
$('#camera-testing')
|
||||
.removeClass('icon-normal')
|
||||
.addClass('icon-blue complete');
|
||||
completedTestingPageIdList.push('camera-testing-body');
|
||||
completedTestingPageIdList = [...new Set(completedTestingPageIdList)];
|
||||
|
||||
await updateCameraDeviceList();
|
||||
|
||||
// 创建本地视频流
|
||||
await createLocalStream(
|
||||
{
|
||||
audio: false,
|
||||
video: true,
|
||||
cameraId: cameraTestingResult.device.deviceId
|
||||
},
|
||||
'camera-video'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化/更新扬声器设备数组
|
||||
*/
|
||||
async function updateVoiceDeviceList() {
|
||||
// 获取扬声器设备并展示在界面中
|
||||
let voiceDevices = await TRTC.getSpeakers();
|
||||
voiceDevices = voiceDevices.filter(voice => voice.deviceId !== 'default');
|
||||
$('#voice-select').empty();
|
||||
voiceDevices.forEach(voice => {
|
||||
let option = $('<option></option>');
|
||||
option.attr('value', voice.deviceId);
|
||||
option.html(voice.label);
|
||||
option.appendTo('#voice-select');
|
||||
});
|
||||
|
||||
// 如果有用户设备选择缓存,优先使用缓存的deviceId
|
||||
let cacheVoiceDevice = voiceDevices.filter(
|
||||
mic => mic.deviceId === localStorage.getItem('txy_webRTC_voiceId')
|
||||
);
|
||||
if (cacheVoiceDevice.length > 0) {
|
||||
$('#voice-select').val(localStorage.getItem('txy_webRTC_voiceId'));
|
||||
voiceTestingResult.device = cacheVoiceDevice[0];
|
||||
} else {
|
||||
$('#voice-select').val(voiceDevices[0].deviceId);
|
||||
voiceTestingResult.device = voiceDevices[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放器设备测试
|
||||
*/
|
||||
async function startVoiceTesting() {
|
||||
$('#voice-testing-body').show();
|
||||
curTestingPageId = 'voice-testing-body';
|
||||
$('#voice-testing')
|
||||
.removeClass('icon-gray')
|
||||
.addClass('icon-blue complete');
|
||||
completedTestingPageIdList.push('voice-testing-body');
|
||||
completedTestingPageIdList = [...new Set(completedTestingPageIdList)];
|
||||
|
||||
await updateVoiceDeviceList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新/初始化麦克风设备
|
||||
*/
|
||||
async function updateMicDeviceList() {
|
||||
// 展示麦克风设备选择
|
||||
let micDevices = await TRTC.getMicrophones();
|
||||
micDevices = micDevices.filter(mic => mic.deviceId !== 'default');
|
||||
$('#mic-select').empty();
|
||||
micDevices.forEach(mic => {
|
||||
let option = $('<option></option>');
|
||||
option.attr('value', mic.deviceId);
|
||||
option.html(mic.label);
|
||||
option.appendTo('#mic-select');
|
||||
});
|
||||
|
||||
// 如果有用户设备选择缓存,优先使用缓存的deviceId
|
||||
let cacheMicDevice = micDevices.filter(
|
||||
mic => mic.deviceId === localStorage.getItem('txy_webRTC_micId')
|
||||
);
|
||||
if (cacheMicDevice.length > 0) {
|
||||
$('#mic-select').val(localStorage.getItem('txy_webRTC_micId'));
|
||||
micTestingResult.device = cacheMicDevice[0];
|
||||
} else {
|
||||
$('#mic-select').val(micDevices[0].deviceId);
|
||||
micTestingResult.device = micDevices[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 麦克风设备测试
|
||||
*/
|
||||
async function startMicTesting() {
|
||||
$('#mic-testing-body').show();
|
||||
curTestingPageId = 'mic-testing-body';
|
||||
$('#mic-testing')
|
||||
.removeClass('icon-gray')
|
||||
.addClass('icon-blue complete');
|
||||
completedTestingPageIdList.push('mic-testing-body');
|
||||
completedTestingPageIdList = [...new Set(completedTestingPageIdList)];
|
||||
|
||||
await updateMicDeviceList();
|
||||
|
||||
// 展示麦克风的声音大小显示
|
||||
if ($('#mic-bar-container').children().length === 0) {
|
||||
for (let index = 0; index < 28; index++) {
|
||||
$('<div></div>')
|
||||
.addClass('mic-bar')
|
||||
.appendTo('#mic-bar-container');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建本地音频流
|
||||
await createLocalStream(
|
||||
{
|
||||
audio: true,
|
||||
microphoneId: micTestingResult.device.deviceId,
|
||||
video: false
|
||||
},
|
||||
'audio-container'
|
||||
);
|
||||
|
||||
// 监听音量,并量化显示出来
|
||||
setInterval(() => {
|
||||
let volume = localStream.getAudioLevel();
|
||||
let num = Math.ceil(28 * volume);
|
||||
$('#mic-bar-container')
|
||||
.children('.active')
|
||||
.removeClass('active');
|
||||
for (let i = 0; i < num; i++) {
|
||||
$('#mic-bar-container')
|
||||
.children()
|
||||
.slice(0, i)
|
||||
.addClass('active');
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统信息展示
|
||||
*/
|
||||
async function startNetworkTesting() {
|
||||
$('#network-testing-body').show();
|
||||
$('#testing-report-btn').hide();
|
||||
curTestingPageId = 'network-testing-body';
|
||||
$('#network-testing')
|
||||
.removeClass('icon-gray')
|
||||
.addClass('icon-blue complete');
|
||||
completedTestingPageIdList.push('network-testing-body');
|
||||
completedTestingPageIdList = [...new Set(completedTestingPageIdList)];
|
||||
|
||||
networkQualityNum = 0;
|
||||
$('#uplink-network')
|
||||
.addClass('network-loading')
|
||||
.text('');
|
||||
|
||||
// 获取系统信息
|
||||
$('#system').empty();
|
||||
let OSInfo = getOS();
|
||||
$('<div></div>')
|
||||
.text(OSInfo.osName)
|
||||
.appendTo('#system');
|
||||
|
||||
// 获取浏览器及版本信息
|
||||
$('#browser').empty();
|
||||
let browser = getBroswer();
|
||||
$('<div></div>')
|
||||
.text(`${browser.broswer} ${browser.version}`)
|
||||
.appendTo('#browser');
|
||||
|
||||
// 获取ip地址信息
|
||||
// $('#ip').empty();
|
||||
// let IPAddress = await getIPAddress();
|
||||
// $('<div></div>').text(IPAddress).appendTo('#ip');
|
||||
// networkTestingResult.IPAddress = IPAddress;
|
||||
|
||||
// 是否支持屏幕分享能力
|
||||
$('#screen-share').empty();
|
||||
let isScreenShareSupported = TRTC.isScreenShareSupported();
|
||||
$('<div></div>')
|
||||
.text(isScreenShareSupported ? '支持' : '不支持')
|
||||
.appendTo('#screen-share');
|
||||
|
||||
// 上下行网络质量
|
||||
presetting.login(false, async options => {
|
||||
client = TRTC.createClient({ mode: 'rtc', ...options });
|
||||
client.on('network-quality', event => {
|
||||
networkQualityNum++;
|
||||
// 收到3次'network-quality'事件的时候认为拿到了网络实际质量
|
||||
if (networkQualityNum === 3) {
|
||||
networkTestingResult.upLinkNetwork = event.uplinkNetworkQuality;
|
||||
networkTestingResult.downLinkNetwork = event.downlinkNetworkQuality;
|
||||
$('#uplink-network')
|
||||
.removeClass('network-loading')
|
||||
.text(NETWORK_QUALITY[String(networkTestingResult.upLinkNetwork)]);
|
||||
$('#testing-report-btn').show();
|
||||
client && client.leave();
|
||||
client && client.off('network-quality');
|
||||
}
|
||||
});
|
||||
await client.join({
|
||||
roomId: options.roomId
|
||||
});
|
||||
await createLocalStream(
|
||||
{
|
||||
audio: true,
|
||||
video: false
|
||||
},
|
||||
'audio-container'
|
||||
);
|
||||
await client.publish(localStream);
|
||||
// 音频轨道静音
|
||||
localStream.muteAudio();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 展示检测报告
|
||||
*/
|
||||
function showTestingReport() {
|
||||
$('#device-testing').hide();
|
||||
$('#network-testing-body').hide();
|
||||
$('#device-testing-report').show();
|
||||
curTestingPageId = 'device-testing-report';
|
||||
|
||||
// 摄像头检测结果
|
||||
$('#camera-name').text(cameraTestingResult.device.label);
|
||||
if (cameraTestingResult.statusResult) {
|
||||
$('#camera-testing-result')
|
||||
.text('正常')
|
||||
.css('color', 'green');
|
||||
} else {
|
||||
$('#camera-testing-result')
|
||||
.text('异常')
|
||||
.css('color', 'red');
|
||||
}
|
||||
|
||||
// 扬声器检测结果(safari浏览器不显示扬声器检测结果)
|
||||
if (!isSafari) {
|
||||
$('#voice-name').text(voiceTestingResult.device.label);
|
||||
if (voiceTestingResult.statusResult) {
|
||||
$('#voice-testing-result')
|
||||
.text('正常')
|
||||
.css('color', 'green');
|
||||
} else {
|
||||
$('#voice-testing-result')
|
||||
.text('异常')
|
||||
.css('color', 'red');
|
||||
}
|
||||
}
|
||||
|
||||
// 麦克风检测结果
|
||||
$('#mic-name').text(micTestingResult.device.label);
|
||||
if (micTestingResult.statusResult) {
|
||||
$('#mic-testing-result')
|
||||
.text('正常')
|
||||
.css('color', 'green');
|
||||
} else {
|
||||
$('#mic-testing-result')
|
||||
.text('异常')
|
||||
.css('color', 'red');
|
||||
}
|
||||
|
||||
// 网络检测结果
|
||||
// $('#network-name').text(networkTestingResult.IPAddress);
|
||||
$('#network-name').text('网络质量');
|
||||
$('#network-testing-result')
|
||||
.html(`${NETWORK_QUALITY[String(networkTestingResult.upLinkNetwork)]}`)
|
||||
.css('color', `${Number(networkTestingResult.upLinkNetwork) > 3 ? 'red' : 'green'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束设备检测,隐藏设备检测弹窗
|
||||
*/
|
||||
function finishDeviceTesting() {
|
||||
$('#device-testing-root').hide();
|
||||
$('#device-testing').hide();
|
||||
$(`#${curTestingPageId}`).hide();
|
||||
curTestingPageId = '';
|
||||
completedTestingPageIdList = [];
|
||||
|
||||
// 停止摄像头/麦克风的流采集并释放摄像头/麦克风设备
|
||||
localStream && localStream.close();
|
||||
client && client.leave();
|
||||
client && client.off('network-quality');
|
||||
// 停止播放器的音乐
|
||||
let audioPlayer = document.querySelector('#audio-player');
|
||||
if (!audioPlayer.paused) {
|
||||
audioPlayer.pause();
|
||||
}
|
||||
audioPlayer.currentTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复检测页面头部图标的状态
|
||||
*/
|
||||
function initTestingTabTitle() {
|
||||
['camera', 'voice', 'mic', 'network'].forEach(item => {
|
||||
$(`#${item}-testing`)
|
||||
.removeClass('icon-blue complete')
|
||||
.addClass('icon-gray');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听设备变化
|
||||
*/
|
||||
navigator.mediaDevices.ondevicechange = async function(event) {
|
||||
// 当前在摄像头检测页
|
||||
if (curTestingPageId === 'camera-testing-body') {
|
||||
await updateCameraDeviceList();
|
||||
return;
|
||||
}
|
||||
// 当前在扬声器检测页
|
||||
if (curTestingPageId === 'voice-testing-body') {
|
||||
await updateVoiceDeviceList();
|
||||
return;
|
||||
}
|
||||
// 当前在麦克风检测页
|
||||
if (curTestingPageId === 'mic-testing-body') {
|
||||
await updateMicDeviceList();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 抽离createStream的公共处理函数
|
||||
*/
|
||||
async function createLocalStream(constraints, container) {
|
||||
localStream = TRTC.createStream(constraints);
|
||||
try {
|
||||
await localStream.initialize();
|
||||
} catch (error) {
|
||||
switch (error.name) {
|
||||
case 'NotReadableError':
|
||||
// 当系统或浏览器异常的时候,可能会出现此错误,您可能需要引导用户重启电脑/浏览器来尝试恢复。
|
||||
alert('暂时无法访问摄像头/麦克风,请确保当前没有其他应用请求访问摄像头/麦克风,并重试');
|
||||
return;
|
||||
case 'NotAllowedError':
|
||||
// 用户拒绝授权访问摄像头或麦克风 | 屏幕分享,您需要引导客户来授权访问
|
||||
alert('用户已拒绝授权访问摄像头或麦克风');
|
||||
return;
|
||||
case 'NotFoundError':
|
||||
// 找不到摄像头或麦克风设备
|
||||
alert('找不到摄像头或麦克风设备');
|
||||
return;
|
||||
case 'OverConstrainedError':
|
||||
alert(
|
||||
'采集属性设置错误,如果您指定了 cameraId/microphoneId,请确保它们是一个有效的非空字符串'
|
||||
);
|
||||
return;
|
||||
default:
|
||||
alert('未知错误');
|
||||
return;
|
||||
}
|
||||
}
|
||||
container && localStream.play(container);
|
||||
}
|
1
public/js/iconfont.js
Normal file
51
public/js/index.js
Normal file
@ -0,0 +1,51 @@
|
||||
/* eslint-disable no-global-assign */
|
||||
/* global $ TRTC Presetting deviceTestingInit cameraId micId */
|
||||
const presetting = new Presetting();
|
||||
presetting.init();
|
||||
deviceTestingInit();
|
||||
|
||||
// check if browser is compatible with TRTC
|
||||
TRTC.checkSystemRequirements().then(result => {
|
||||
if (!result) {
|
||||
alert('您的浏览器不兼容此应用!\n建议下载最新版Chrome浏览器');
|
||||
window.location.href = 'http://www.google.cn/chrome/';
|
||||
}
|
||||
});
|
||||
|
||||
// setup logging stuffs
|
||||
TRTC.Logger.setLogLevel(TRTC.Logger.LogLevel.DEBUG);
|
||||
TRTC.Logger.enableUploadLog();
|
||||
|
||||
TRTC.getDevices()
|
||||
.then(devices => {
|
||||
devices.forEach(item => {
|
||||
console.log('device: ' + item.kind + ' ' + item.label + ' ' + item.deviceId);
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('getDevices error observed ' + error));
|
||||
|
||||
// populate camera options
|
||||
TRTC.getCameras().then(devices => {
|
||||
devices.forEach(device => {
|
||||
if (!cameraId) {
|
||||
cameraId = device.deviceId;
|
||||
}
|
||||
let div = $('<div></div>');
|
||||
div.attr('id', device.deviceId);
|
||||
div.html(device.label);
|
||||
div.appendTo('#camera-option');
|
||||
});
|
||||
});
|
||||
|
||||
// populate microphone options
|
||||
TRTC.getMicrophones().then(devices => {
|
||||
devices.forEach(device => {
|
||||
if (!micId) {
|
||||
micId = device.deviceId;
|
||||
}
|
||||
let div = $('<div></div>');
|
||||
div.attr('id', device.deviceId);
|
||||
div.html(device.label);
|
||||
div.appendTo('#mic-option');
|
||||
});
|
||||
});
|
4
public/js/jquery-3.2.1.min.js
vendored
Normal file
2
public/js/lib-generate-test-usersig.min.js
vendored
Normal file
2442
public/js/popper.js
Normal file
45
public/js/presetting.js
Normal file
@ -0,0 +1,45 @@
|
||||
/* global $ setBtnClickFuc genTestUserSig */
|
||||
// preset before starting RTC
|
||||
class Presetting {
|
||||
init() {
|
||||
// populate userId/roomId
|
||||
$('#userId').val('user_30400097');
|
||||
$('#roomId').val(parseInt(Math.random() * 100000));
|
||||
const roomId = this.query('roomId');
|
||||
const userId = this.query('userId');
|
||||
if (roomId) {
|
||||
$('#roomId').val(roomId);
|
||||
}
|
||||
if (userId) {
|
||||
$('#userId').val(userId);
|
||||
}
|
||||
|
||||
$('#main-video-btns').hide();
|
||||
$('.mask').hide();
|
||||
setBtnClickFuc();
|
||||
}
|
||||
|
||||
query(name) {
|
||||
const match = window.location.search.match(new RegExp('(\\?|&)' + name + '=([^&]*)(&|$)'));
|
||||
return !match ? '' : decodeURIComponent(match[2]);
|
||||
}
|
||||
|
||||
login(share, callback) {
|
||||
let userId = $('#userId').val();
|
||||
if (share) {
|
||||
userId = 'share_' + userId;
|
||||
}
|
||||
console.log(userId)
|
||||
const config = genTestUserSig(userId);
|
||||
const sdkAppId = config.sdkAppId;
|
||||
const userSig = config.userSig;
|
||||
const roomId = $('#roomId').val();
|
||||
|
||||
callback({
|
||||
sdkAppId,
|
||||
userId,
|
||||
userSig,
|
||||
roomId
|
||||
});
|
||||
}
|
||||
}
|
341
public/js/rtc-client.js
Normal file
@ -0,0 +1,341 @@
|
||||
/* global $ TRTC getCameraId getMicrophoneId resetView isHidden shareUserId addMemberView removeView addVideoView */
|
||||
class RtcClient {
|
||||
constructor(options) {
|
||||
this.sdkAppId_ = options.sdkAppId;
|
||||
this.userId_ = options.userId;
|
||||
this.userSig_ = options.userSig;
|
||||
this.roomId_ = options.roomId;
|
||||
|
||||
this.isJoined_ = false;
|
||||
this.isPublished_ = false;
|
||||
this.isAudioMuted = false;
|
||||
this.isVideoMuted = false;
|
||||
this.localStream_ = null;
|
||||
this.remoteStreams_ = [];
|
||||
this.members_ = new Map();
|
||||
|
||||
// create a client for RtcClient
|
||||
this.client_ = TRTC.createClient({
|
||||
mode: 'rtc',
|
||||
sdkAppId: this.sdkAppId_,
|
||||
userId: this.userId_,
|
||||
userSig: this.userSig_
|
||||
});
|
||||
this.handleEvents();
|
||||
}
|
||||
|
||||
async join() {
|
||||
if (this.isJoined_) {
|
||||
console.warn('duplicate RtcClient.join() observed');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// join the room
|
||||
await this.client_.join({
|
||||
roomId: this.roomId_
|
||||
});
|
||||
console.log('join room success');
|
||||
this.isJoined_ = true;
|
||||
|
||||
// create a local stream with audio/video from microphone/camera
|
||||
if (getCameraId() && getMicrophoneId()) {
|
||||
this.localStream_ = TRTC.createStream({
|
||||
audio: true,
|
||||
video: true,
|
||||
userId: this.userId_,
|
||||
cameraId: getCameraId(),
|
||||
microphoneId: getMicrophoneId(),
|
||||
mirror: true
|
||||
});
|
||||
} else {
|
||||
// not to specify cameraId/microphoneId to avoid OverConstrainedError
|
||||
this.localStream_ = TRTC.createStream({
|
||||
audio: true,
|
||||
video: true,
|
||||
userId: this.userId_,
|
||||
mirror: true
|
||||
});
|
||||
}
|
||||
try {
|
||||
// initialize the local stream and the stream will be populated with audio/video
|
||||
await this.localStream_.initialize();
|
||||
console.log('initialize local stream success');
|
||||
|
||||
this.localStream_.on('player-state-changed', event => {
|
||||
console.log(`local stream ${event.type} player is ${event.state}`);
|
||||
});
|
||||
|
||||
// publish the local stream
|
||||
await this.publish();
|
||||
|
||||
this.localStream_.play('main-video');
|
||||
$('#main-video-btns').show();
|
||||
$('#mask_main').appendTo($('#player_' + this.localStream_.getId()));
|
||||
} catch (e) {
|
||||
console.error('failed to initialize local stream - ' + e);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('join room failed! ' + e);
|
||||
}
|
||||
//更新成员状态
|
||||
let states = this.client_.getRemoteMutedState();
|
||||
for (let state of states) {
|
||||
if (state.audioMuted) {
|
||||
$('#' + state.userId)
|
||||
.find('.member-audio-btn')
|
||||
.attr('src', './img/mic-off.png');
|
||||
}
|
||||
if (state.videoMuted) {
|
||||
$('#' + state.userId)
|
||||
.find('.member-video-btn')
|
||||
.attr('src', './img/camera-off.png');
|
||||
$('#mask_' + this.members_.get(state.userId).getId()).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async leave() {
|
||||
if (!this.isJoined_) {
|
||||
console.warn('leave() - please join() firstly');
|
||||
return;
|
||||
}
|
||||
// ensure the local stream is unpublished before leaving.
|
||||
await this.unpublish();
|
||||
|
||||
// leave the room
|
||||
await this.client_.leave();
|
||||
|
||||
this.localStream_.stop();
|
||||
this.localStream_.close();
|
||||
this.localStream_ = null;
|
||||
this.isJoined_ = false;
|
||||
resetView();
|
||||
}
|
||||
|
||||
async publish() {
|
||||
if (!this.isJoined_) {
|
||||
console.warn('publish() - please join() firstly');
|
||||
return;
|
||||
}
|
||||
if (this.isPublished_) {
|
||||
console.warn('duplicate RtcClient.publish() observed');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.client_.publish(this.localStream_);
|
||||
} catch (e) {
|
||||
console.error('failed to publish local stream ' + e);
|
||||
this.isPublished_ = false;
|
||||
}
|
||||
|
||||
this.isPublished_ = true;
|
||||
}
|
||||
|
||||
async unpublish() {
|
||||
if (!this.isJoined_) {
|
||||
console.warn('unpublish() - please join() firstly');
|
||||
return;
|
||||
}
|
||||
if (!this.isPublished_) {
|
||||
console.warn('RtcClient.unpublish() called but not published yet');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.client_.unpublish(this.localStream_);
|
||||
this.isPublished_ = false;
|
||||
}
|
||||
|
||||
muteLocalAudio() {
|
||||
this.localStream_.muteAudio();
|
||||
}
|
||||
|
||||
unmuteLocalAudio() {
|
||||
this.localStream_.unmuteAudio();
|
||||
}
|
||||
|
||||
muteLocalVideo() {
|
||||
this.localStream_.muteVideo();
|
||||
}
|
||||
|
||||
unmuteLocalVideo() {
|
||||
this.localStream_.unmuteVideo();
|
||||
}
|
||||
|
||||
resumeStreams() {
|
||||
this.localStream_.resume();
|
||||
for (let stream of this.remoteStreams_) {
|
||||
stream.resume();
|
||||
}
|
||||
}
|
||||
|
||||
handleEvents() {
|
||||
this.client_.on('error', err => {
|
||||
console.error(err);
|
||||
alert(err);
|
||||
location.reload();
|
||||
});
|
||||
this.client_.on('client-banned', err => {
|
||||
console.error('client has been banned for ' + err);
|
||||
if (!isHidden()) {
|
||||
alert('您已被踢出房间');
|
||||
location.reload();
|
||||
} else {
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
() => {
|
||||
if (!isHidden()) {
|
||||
alert('您已被踢出房间');
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
// fired when a remote peer is joining the room
|
||||
this.client_.on('peer-join', evt => {
|
||||
const userId = evt.userId;
|
||||
console.log('peer-join ' + userId);
|
||||
if (userId !== shareUserId) {
|
||||
addMemberView(userId);
|
||||
}
|
||||
});
|
||||
// fired when a remote peer is leaving the room
|
||||
this.client_.on('peer-leave', evt => {
|
||||
const userId = evt.userId;
|
||||
removeView(userId);
|
||||
console.log('peer-leave ' + userId);
|
||||
});
|
||||
// fired when a remote stream is added
|
||||
this.client_.on('stream-added', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
const id = remoteStream.getId();
|
||||
const userId = remoteStream.getUserId();
|
||||
this.members_.set(userId, remoteStream);
|
||||
console.log(`remote stream added: [${userId}] ID: ${id} type: ${remoteStream.getType()}`);
|
||||
if (remoteStream.getUserId() === shareUserId) {
|
||||
// don't need screen shared by us
|
||||
this.client_.unsubscribe(remoteStream);
|
||||
} else {
|
||||
console.log('subscribe to this remote stream');
|
||||
this.client_.subscribe(remoteStream);
|
||||
}
|
||||
});
|
||||
// fired when a remote stream has been subscribed
|
||||
this.client_.on('stream-subscribed', evt => {
|
||||
const uid = evt.userId;
|
||||
const remoteStream = evt.stream;
|
||||
const id = remoteStream.getId();
|
||||
this.remoteStreams_.push(remoteStream);
|
||||
remoteStream.on('player-state-changed', event => {
|
||||
console.log(`${event.type} player is ${event.state}`);
|
||||
if (event.type == 'video' && event.state == 'STOPPED') {
|
||||
$('#mask_' + remoteStream.getId()).show();
|
||||
$('#' + remoteStream.getUserId())
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-off.png');
|
||||
}
|
||||
if (event.type == 'video' && event.state == 'PLAYING') {
|
||||
$('#mask_' + remoteStream.getId()).hide();
|
||||
$('#' + remoteStream.getUserId())
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-on.png');
|
||||
}
|
||||
});
|
||||
addVideoView(id);
|
||||
// objectFit 为播放的填充模式,详细参考:https://trtc-1252463788.file.myqcloud.com/web/docs/Stream.html#play
|
||||
remoteStream.play(id, { objectFit: 'contain' });
|
||||
//添加“摄像头未打开”遮罩
|
||||
let mask = $('#mask_main').clone();
|
||||
mask.attr('id', 'mask_' + id);
|
||||
mask.appendTo($('#player_' + id));
|
||||
mask.hide();
|
||||
if (!remoteStream.hasVideo()) {
|
||||
mask.show();
|
||||
$('#' + remoteStream.getUserId())
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-off.png');
|
||||
}
|
||||
console.log('stream-subscribed ID: ', id);
|
||||
});
|
||||
// fired when the remote stream is removed, e.g. the remote user called Client.unpublish()
|
||||
this.client_.on('stream-removed', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
const id = remoteStream.getId();
|
||||
remoteStream.stop();
|
||||
this.remoteStreams_ = this.remoteStreams_.filter(stream => {
|
||||
return stream.getId() !== id;
|
||||
});
|
||||
removeView(id);
|
||||
console.log(`stream-removed ID: ${id} type: ${remoteStream.getType()}`);
|
||||
});
|
||||
|
||||
this.client_.on('stream-updated', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
let uid = this.getUidByStreamId(remoteStream.getId());
|
||||
if (!remoteStream.hasVideo()) {
|
||||
$('#' + uid)
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-off.png');
|
||||
}
|
||||
console.log(
|
||||
'type: ' +
|
||||
remoteStream.getType() +
|
||||
' stream-updated hasAudio: ' +
|
||||
remoteStream.hasAudio() +
|
||||
' hasVideo: ' +
|
||||
remoteStream.hasVideo() +
|
||||
' uid: ' +
|
||||
uid
|
||||
);
|
||||
});
|
||||
|
||||
this.client_.on('mute-audio', evt => {
|
||||
console.log(evt.userId + ' mute audio');
|
||||
$('#' + evt.userId)
|
||||
.find('.member-audio-btn')
|
||||
.attr('src', 'img/mic-off.png');
|
||||
});
|
||||
this.client_.on('unmute-audio', evt => {
|
||||
console.log(evt.userId + ' unmute audio');
|
||||
$('#' + evt.userId)
|
||||
.find('.member-audio-btn')
|
||||
.attr('src', 'img/mic-on.png');
|
||||
});
|
||||
this.client_.on('mute-video', evt => {
|
||||
console.log(evt.userId + ' mute video');
|
||||
$('#' + evt.userId)
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-off.png');
|
||||
let streamId = this.members_.get(evt.userId).getId();
|
||||
if (streamId) {
|
||||
$('#mask_' + streamId).show();
|
||||
}
|
||||
});
|
||||
this.client_.on('unmute-video', evt => {
|
||||
console.log(evt.userId + ' unmute video');
|
||||
$('#' + evt.userId)
|
||||
.find('.member-video-btn')
|
||||
.attr('src', 'img/camera-on.png');
|
||||
const stream = this.members_.get(evt.userId);
|
||||
if (stream) {
|
||||
let streamId = stream.getId();
|
||||
if (streamId) {
|
||||
$('#mask_' + streamId).hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showStreamState(stream) {
|
||||
console.log('has audio: ' + stream.hasAudio() + ' has video: ' + stream.hasVideo());
|
||||
}
|
||||
|
||||
getUidByStreamId(streamId) {
|
||||
for (let [uid, stream] of this.members_) {
|
||||
if (stream.getId() == streamId) {
|
||||
return uid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
163
public/js/share-client.js
Normal file
@ -0,0 +1,163 @@
|
||||
/* global $ TRTC */
|
||||
class ShareClient {
|
||||
constructor(options) {
|
||||
this.sdkAppId_ = options.sdkAppId;
|
||||
this.userId_ = options.userId;
|
||||
this.userSig_ = options.userSig;
|
||||
this.roomId_ = options.roomId;
|
||||
|
||||
this.isJoined_ = false;
|
||||
this.isPublished_ = false;
|
||||
this.localStream_ = null;
|
||||
|
||||
this.client_ = TRTC.createClient({
|
||||
mode: 'rtc',
|
||||
sdkAppId: this.sdkAppId_,
|
||||
userId: this.userId_,
|
||||
userSig: this.userSig_,
|
||||
/**
|
||||
* disable receivers to avoid receiving remote streams as we only want to
|
||||
* publish the screen stream
|
||||
*/
|
||||
disableReceiver: true
|
||||
});
|
||||
|
||||
this.client_.setDefaultMuteRemoteStreams(true);
|
||||
this.handleEvents();
|
||||
}
|
||||
|
||||
async join() {
|
||||
if (this.isJoined_) {
|
||||
console.warn('duplicate RtcClient.join() observed');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.client_.join({
|
||||
roomId: this.roomId_
|
||||
});
|
||||
console.log('ShareClient join room success');
|
||||
this.isJoined_ = true;
|
||||
|
||||
// create a local stream for screen share
|
||||
this.localStream_ = TRTC.createStream({
|
||||
// disable audio as RtcClient already enable audio
|
||||
audio: false,
|
||||
// enable screen share
|
||||
screen: true,
|
||||
userId: this.userId_
|
||||
});
|
||||
try {
|
||||
// initialize the local stream to populate the screen stream
|
||||
await this.localStream_.initialize();
|
||||
console.log('ShareClient initialize local stream for screen share success');
|
||||
|
||||
this.localStream_.on('player-state-changed', event => {
|
||||
console.log(`local stream ${event.type} player is ${event.state}`);
|
||||
});
|
||||
this.localStream_.on('screen-sharing-stopped', event => {
|
||||
console.log('share stream video track enned');
|
||||
this.leave();
|
||||
$('#screen-btn').attr('src', './img/screen-off.png');
|
||||
});
|
||||
|
||||
// publish the screen share stream
|
||||
await this.client_.publish(this.localStream_);
|
||||
} catch (e) {
|
||||
console.error('ShareClient failed to initialize local stream - ' + e);
|
||||
//用户取消分享屏幕导致推流失败
|
||||
await this.client_.leave();
|
||||
this.isJoined_ = false;
|
||||
$('#screen-btn').attr('src', 'img/screen-off.png');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ShareClient join room failed! ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
async leave() {
|
||||
if (!this.isJoined_) {
|
||||
console.warn('leave() - please join() firstly');
|
||||
return;
|
||||
}
|
||||
if (this.isPublished_) {
|
||||
await this.client_.unpublish(this.localStream_);
|
||||
this.isPublished_ = false;
|
||||
}
|
||||
await this.client_.leave();
|
||||
if (this.localStream_) {
|
||||
this.localStream_.close();
|
||||
this.localStream_ = null;
|
||||
}
|
||||
this.isJoined_ = false;
|
||||
}
|
||||
|
||||
handleEvents() {
|
||||
this.client_.on('error', err => {
|
||||
console.error(err);
|
||||
alert(err);
|
||||
});
|
||||
this.client_.on('client-banned', err => {
|
||||
console.error('client has been banned for ' + err);
|
||||
});
|
||||
// fired when a remote peer is joining the room
|
||||
this.client_.on('peer-join', evt => {
|
||||
const userId = evt.userId;
|
||||
console.log('peer-join ' + userId);
|
||||
});
|
||||
// fired when a remote peer is leaving the room
|
||||
this.client_.on('peer-leave', evt => {
|
||||
const userId = evt.userId;
|
||||
console.log('peer-leave ' + userId);
|
||||
});
|
||||
// fired when a remote stream is added
|
||||
this.client_.on('stream-added', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
const id = remoteStream.getId();
|
||||
const userId = remoteStream.getUserId();
|
||||
console.log(`remote stream added: [${userId}] ID: ${id} type: ${remoteStream.getType()}`);
|
||||
console.log('subscribe to this remote stream');
|
||||
});
|
||||
// fired when a remote stream has been subscribed
|
||||
this.client_.on('stream-subscribed', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
const id = remoteStream.getId();
|
||||
remoteStream.on('player-state-changed', event => {
|
||||
console.log(`${event.type} player is ${event.state}`);
|
||||
});
|
||||
console.log('stream-subscribed ID: ', id);
|
||||
});
|
||||
// fired when the remote stream is removed, e.g. the remote user called Client.unpublish()
|
||||
this.client_.on('stream-removed', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
const id = remoteStream.getId();
|
||||
console.log(`stream-removed ID: ${id} type: ${remoteStream.getType()}`);
|
||||
});
|
||||
|
||||
this.client_.on('stream-updated', evt => {
|
||||
const remoteStream = evt.stream;
|
||||
console.log(
|
||||
'type: ' +
|
||||
remoteStream.getType() +
|
||||
' stream-updated hasAudio: ' +
|
||||
remoteStream.hasAudio() +
|
||||
' hasVideo: ' +
|
||||
remoteStream.hasVideo() +
|
||||
' uid: ' +
|
||||
remoteStream.getUserId()
|
||||
);
|
||||
});
|
||||
|
||||
this.client_.on('mute-audio', evt => {
|
||||
console.log(evt.userId + ' mute audio');
|
||||
});
|
||||
this.client_.on('unmute-audio', evt => {
|
||||
console.log(evt.userId + ' unmute audio');
|
||||
});
|
||||
this.client_.on('mute-video', evt => {
|
||||
console.log(evt.userId + ' mute video');
|
||||
});
|
||||
this.client_.on('unmute-video', evt => {
|
||||
console.log(evt.userId + ' unmute video');
|
||||
});
|
||||
}
|
||||
}
|
1
public/js/trtc.js
Normal file
431
public/zhibo.html
Normal file
@ -0,0 +1,431 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>TRTC实时音视频通话</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7, user-scalable=no, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="./css/bootstrap-material-design.min.css">
|
||||
<link rel="stylesheet" href="./css/index.css">
|
||||
<link rel="stylesheet" href="./css/room.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
<!-- 登录页面 -->
|
||||
<div id="login-root">
|
||||
<!-- 登录卡片 -->
|
||||
<div id="login-card" class="card">
|
||||
<!-- 顶部三个蓝条 -->
|
||||
<div class="row-div" style="width: 100%; height: 10px">
|
||||
<div style="width: 190px; height: 100%; background-color: #006EFF"></div>
|
||||
<div style="width: 160px; height: 100%; background-color: #00A4FF"></div>
|
||||
<div style="width: 100px; height: 100%; background-color: #5AD5E0"></div>
|
||||
</div>
|
||||
<!-- 腾讯云logo -->
|
||||
<div class="row-div" style="width: 100%; height: 100px; justify-content: center">
|
||||
<img style="height: 23px" src="./img/logo.png" alt="">
|
||||
<div style="width: 9px"></div>
|
||||
<div style="width: 1px; height: 10px; background-color: #D8D8D8"></div>
|
||||
<div style="width: 9px"></div>
|
||||
<div style="width: 86px; height: 23px; font-size: 18px; color: #333333">视频通话</div>
|
||||
</div>
|
||||
<!-- 用户名 房间号 登录按钮-->
|
||||
<div class="col-div" style="width: 320px">
|
||||
<div class="form-group bmd-form-group is-filled" style="width: 100%; height: 80px">
|
||||
<label for="userId" class="bmd-label-floating">用户名:</label>
|
||||
<input type="text" class="form-control" name="userId" id="userId" maxlength="18">
|
||||
</div>
|
||||
<div class="form-group bmd-form-group is-filled" style="width: 100%; height: 80px">
|
||||
<label for="roomId" class="bmd-label-floating">房间号:</label>
|
||||
<input type="text" class="form-control" name="roomId" id="roomId" maxlength="18">
|
||||
</div>
|
||||
<div style="height: 24px"></div>
|
||||
<!-- 登录 -->
|
||||
<button id="login-btn" type="button" class="btn btn-raised btn-primary"
|
||||
style="width: 100%; height: 40px">进入房间
|
||||
<div class="ripple-container"></div>
|
||||
</button>
|
||||
<!-- 摄像头 麦克风 -->
|
||||
<div class="row-div" style="width: 100%; height: 105px; justify-content: center">
|
||||
<img id="camera" style="height: 27px" src="./img/camera.png" onClick="event.cancelBubble = true"
|
||||
data-toggle="popover" data-placement="top" title="" data-content="">
|
||||
<!-- 选择摄像头 -->
|
||||
<div id="camera-option" style="display: none"></div>
|
||||
<div style="width: 100px"></div>
|
||||
<img id="microphone" style="height: 27px" src="./img/mic.png"
|
||||
onClick="event.cancelBubble = true" data-toggle="popover" data-placement="top" title=""
|
||||
data-content="">
|
||||
<!-- 选择麦克风 -->
|
||||
<div id="mic-option" style="display: none"></div>
|
||||
</div>
|
||||
<!-- 设备检测按钮 -->
|
||||
<div id="device-testing-btn" class="device-testing-btn">
|
||||
<div class="device-icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shebei"></use>
|
||||
</svg>
|
||||
</div>
|
||||
设备检测
|
||||
</div>
|
||||
<div id="device-connect-list" class="device-connect-list" style="display: none;">
|
||||
<div id="connect-camera" class="connect icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shiping-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="connect-voice" class="connect icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shengyin"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="connect-mic" class="connect icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-maikefeng-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="connect-network" class="connect icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-wangluo"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测界面弹窗 -->
|
||||
<div id="device-testing-root" style="display: none;">
|
||||
<!-- 设备检测卡片 -->
|
||||
<div class="device-testing-card">
|
||||
<!-- 设备检测准备界面 -->
|
||||
<div id="device-testing-prepare" class="device-testing-prepare">
|
||||
<div class="testing-title">设备连接</div>
|
||||
<div class="testing-prepare-info">设备检测前请务必给当前页面开放摄像头,麦克风权限哦~</div>
|
||||
<div class="device-display">
|
||||
<div id="device-camera" class="device icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shiping-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="device-voice" class="device icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shengyin"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="device-mic" class="device icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-maikefeng-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="device-network" class="device icon-normal">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-wangluo"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div id="device-loading" class="loading-background">
|
||||
<div class="device-loading"></div>
|
||||
</div>
|
||||
<!-- 连接结果提示 -->
|
||||
<div class="connect-info">
|
||||
<!-- 连接结果 -->
|
||||
<div id="connect-info"></div>
|
||||
<!-- 错误icon及错误解决指引 -->
|
||||
<div id="connect-attention-container" class="connect-attention-container" style="display: none;">
|
||||
<div id="connect-attention-icon" class="connect-attention-icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-warn"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="connect-attention-info" class="connect-attention-info" style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备连接页面button -->
|
||||
<div class="testing-btn-display">
|
||||
<div id="start-test-btn" class="test-btn start-test start-gray">开始检测</div>
|
||||
<div id="connect-again-btn" class="test-btn connect-again" style="display: none;">重新连接</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测tab页 -->
|
||||
<div id="device-testing" class="device-testing" style="display: none;">
|
||||
<div class="device-testing-title">
|
||||
<div id="camera-testing" class="testing icon-gray">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shiping-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="voice-testing" class="testing icon-gray">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shengyin"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="mic-testing" class="testing icon-gray">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-maikefeng-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="network-testing" class="testing icon-gray">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-wangluo"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测-摄像头检测 -->
|
||||
<div id="camera-testing-body" class="testing-body" style="display: none;">
|
||||
<div class="device-list camera-device-list">
|
||||
<div class="select-title" style="display: block;">摄像头选择</div>
|
||||
<div class="select-list" style="display: block;">
|
||||
<select name="select" id="camera-select" class="device-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="camera-video" class="camera-video"></div>
|
||||
<div class="testing-info-container">
|
||||
<div class="testing-info">是否可以清楚的看到自己?</div>
|
||||
<div class="button-list">
|
||||
<div id="camera-fail" class="fail-button">看不到</div>
|
||||
<div id="camera-success" class="success-button">可以看到</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测-播放器检测 -->
|
||||
<div id="voice-testing-body" class="testing-body" style="display: none;">
|
||||
<div class="device-list camera-device-list">
|
||||
<div class="select-title" style="display: block;">扬声器选择</div>
|
||||
<div class="select-list" style="display: block;">
|
||||
<select name="select" id="voice-select" class="device-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-control">
|
||||
<div class="audio-control-info">请调高设备音量, 点击播放下面的音频试试~</div>
|
||||
<audio id="audio-player" src="https://trtc-1252463788.cos.ap-guangzhou.myqcloud.com/web/assets/bgm-test.mp3" controls></audio>
|
||||
</div>
|
||||
<div class="testing-info-container">
|
||||
<div class="testing-info">是否可以听到声音?</div>
|
||||
<div class="button-list">
|
||||
<div id="voice-fail" class="fail-button">听不到</div>
|
||||
<div id="voice-success" class="success-button">可以听到</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测-麦克风检测 -->
|
||||
<div id="mic-testing-body" class="testing-body" style="display: none;">
|
||||
<div class="device-list camera-device-list">
|
||||
<div class="select-title" style="display: block;">麦克风选择</div>
|
||||
<div class="select-list" style="display: block;">
|
||||
<select name="select" id="mic-select" class="device-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mic-testing-container">
|
||||
<div class="mic-testing-info">对着麦克风说'哈喽'试试~</div>
|
||||
<div id="mic-bar-container" class="mic-bar-container"></div>
|
||||
<div id="audio-container"></div>
|
||||
</div>
|
||||
<div class="testing-info-container">
|
||||
<div class="testing-info">是否可以看到音量图标跳动?</div>
|
||||
<div class="button-list">
|
||||
<div id="mic-fail" class="fail-button">看不到</div>
|
||||
<div id="mic-success" class="success-button">可以看到</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测-硬件及网速检测 -->
|
||||
<div id="network-testing-body" class="testing-body" style="display: none;">
|
||||
<div class="testing-index-list">
|
||||
<div class="testing-index-group">
|
||||
<div class="testing-index">操作系统</div>
|
||||
<div id="system"></div>
|
||||
</div>
|
||||
<div class="testing-index-group">
|
||||
<div class="testing-index">浏览器版本</div>
|
||||
<div id="browser"></div>
|
||||
</div>
|
||||
<!-- <div class="testing-index-group">
|
||||
<div class="testing-index">IP地址</div>
|
||||
<div id="ip"></div>
|
||||
</div> -->
|
||||
<div class="testing-index-group">
|
||||
<div class="testing-index">屏幕共享能力</div>
|
||||
<div id="screen-share"></div>
|
||||
</div>
|
||||
<div class="testing-index-group">
|
||||
<div class="testing-index">网络质量</div>
|
||||
<div id="uplink-network" class="network-loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="testing-footer">
|
||||
<div id="testing-report-btn" class="test-btn">查看检测报告</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测报告 -->
|
||||
<div id="device-testing-report" class="device-testing-report" style="display: none;">
|
||||
<div class="testing-title">检测报告</div>
|
||||
<!-- 检测报告内容 -->
|
||||
<div class="device-report-list">
|
||||
<!-- 摄像头报告信息 -->
|
||||
<div class="device-report camera-report">
|
||||
<div class="device-info">
|
||||
<div class="report-icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shiping-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="camera-name" class="device-name"></div>
|
||||
</div>
|
||||
<div id="camera-testing-result" class="camera-testing-result"></div>
|
||||
</div>
|
||||
<!-- 扬声器报告信息 -->
|
||||
<div id="voice-report" class="device-report voice-report">
|
||||
<div class="device-info">
|
||||
<div class="report-icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-shengyin"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="voice-name" class="device-name"></div>
|
||||
</div>
|
||||
<div id="voice-testing-result" class="voice-testing-result"></div>
|
||||
</div>
|
||||
<!-- 麦克风报告信息 -->
|
||||
<div class="device-report mic-report">
|
||||
<div class="device-info">
|
||||
<div class="report-icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-maikefeng-xue"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="mic-name" class="device-name"></div>
|
||||
</div>
|
||||
<div id="mic-testing-result" class="mic-testing-result"></div>
|
||||
</div>
|
||||
<!-- 网络报告信息 -->
|
||||
<div class="device-report network-report">
|
||||
<div class="device-info">
|
||||
<div class="report-icon">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-wangluo"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="network-name" class="device-name"></div>
|
||||
</div>
|
||||
<div id="network-testing-result" class="network-testing-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-report-footer">
|
||||
<div id="testing-again" class="device-report-btn testing-agin">重新检测</div>
|
||||
<div id="testing-finish" class="device-report-btn testing-finish">完成检测</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 设备检测关闭按钮 -->
|
||||
<div id="device-testing-close-btn" class="device-testing-close-btn">
|
||||
<svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-baseline-close-px"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 聊天室页面 -->
|
||||
<div id="room-root" class="col-div">
|
||||
<!-- header -->
|
||||
<div class="row-div card" style="width: 100%; height: 65px; justify-content: space-between">
|
||||
<!-- 腾讯云logo -->
|
||||
<div class="row-div" style="height: 100%; width: 230px; justify-content: center">
|
||||
<img style="height: 23px" src="./img/logo.png" alt="">
|
||||
<div style="width: 9px"></div>
|
||||
<div style="width: 1px; height: 10px; background-color: #D8D8D8"></div>
|
||||
<div style="width: 9px"></div>
|
||||
<div style="width: 86px; height: 23px; font-size: 18px; color: #333333">视频通话</div>
|
||||
</div>
|
||||
<!-- 分享屏幕 退出 按钮 -->
|
||||
<div class="row-div" style="height: 100%; width: auto;">
|
||||
<img id="screen-btn" style="width: 65px; height: 65px" src="./img/screen-off.png" alt="">
|
||||
<div style="width: 20px"></div>
|
||||
<img id="logout-btn" style="width: 65px; height: 65px" src="./img/logout.png" alt="">
|
||||
</div>
|
||||
<!-- 房间号 -->
|
||||
<div id="header-roomId"
|
||||
style="width: 230px; justify-content: flex-end; padding-right: 20px; font-size: 14px; color: #888888">
|
||||
房间号: 12345</div>
|
||||
</div>
|
||||
<!-- content -->
|
||||
<div class="row-div" style="height: 100%; width: 100%; padding: 10px">
|
||||
<div class="col-div" style="width: 340px; height: 100%; padding: 10px">
|
||||
<div class="col-div card" style="width: 100%; height: 100%">
|
||||
<!-- 成员列表 -->
|
||||
<div id="member-list" class="col-div" style="width: 100%; justify-content: flex-start; flex: 1">
|
||||
<!-- member -->
|
||||
<div id="member-me" style="width: 100%; padding-left: 20px">
|
||||
<div class="row-div member"
|
||||
style="width: 100%; height: 50px; justify-content: space-between">
|
||||
<div class="member-id">(我)</div>
|
||||
<div class="row-div" style="width:100px; height: 27px; justify-content: center">
|
||||
<img class="member-video-btn" style="height: 100%" src="./img/camera-on.png"
|
||||
alt="">
|
||||
<div style="width: 18px"></div>
|
||||
<img class="member-audio-btn" style="height: 100%" src="./img/mic-on.png"
|
||||
alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 广告 -->
|
||||
<div class="col-div" style="width: 100%; height: 244px">
|
||||
<div style="width: 280px; height: 1px; background-color: #f0f0f0"></div>
|
||||
<div class="row-div" style="width: 100%; height: 186px; justify-content: center">
|
||||
<img style="height: 150px" src="./img/code.jpg" alt="">
|
||||
<div style="width: 46px"></div>
|
||||
<img style="height: 150px" src="./img/shot.png" alt="">
|
||||
</div>
|
||||
<div style="width: 290px; font-size: 14px; color: #888888">微信扫一扫打开小程序,点击“视频通话”即可加入通话
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 视频网格 -->
|
||||
<div id="video-grid" style="height: 100%; flex: 1">
|
||||
<!-- 主视频 -->
|
||||
<div id="main-video" class="video-box col-div" style="justify-content: flex-end">
|
||||
<!-- 主视频控制按钮 -->
|
||||
<div id="main-video-btns" class="row-div"
|
||||
style="width: 156px; position: absolute; z-index: 10; justify-content: center; align-self: flex-end">
|
||||
<img id="video-btn" style="width: 68px; height: 68px" onClick="event.cancelBubble = true"
|
||||
src="./img/big-camera-on.png" alt="" title="关闭摄像头">
|
||||
<img id="mic-btn" style="width: 68px; height: 68px" onClick="event.cancelBubble = true"
|
||||
src="./img/big-mic-on.png" alt="" title="关闭麦克风">
|
||||
</div>
|
||||
<div id="mask_main" class="mask col-div">
|
||||
<!-- “摄像头未开启”遮罩 -->
|
||||
<div style="height: 100%; width: 100%; position: absolute; background-color: #D8D8D8"></div>
|
||||
<img style="width: 63px; height: 69px; z-index: 10;" src="./img/camera-max.png" alt="">
|
||||
<div style="height: 10px"></div>
|
||||
<div style="z-index: 10">摄像头未打开</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 小视频 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="./js/jquery-3.2.1.min.js"></script>
|
||||
<script src="./js/popper.js"></script>
|
||||
<script src="./js/bootstrap-material-design.js"></script>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('body').bootstrapMaterialDesign();
|
||||
});
|
||||
</script>
|
||||
<script src="./js/lib-generate-test-usersig.min.js"></script>
|
||||
<script src="./js/debug/GenerateTestUserSig.js"></script>
|
||||
<script src="./js/iconfont.js"></script>
|
||||
<script src="./js/trtc.js"></script>
|
||||
<script src="./js/common.js"></script>
|
||||
<script src="./js/rtc-client.js"></script>
|
||||
<script src="./js/share-client.js"></script>
|
||||
<script src="./js/presetting.js"></script>
|
||||
<script src="./js/device-testing.js"></script>
|
||||
<script src="./js/index.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,587 +1,19 @@
|
||||
<template>
|
||||
<div class="liveing">
|
||||
|
||||
<div class="top">
|
||||
<div class="left">{{lan.$t('zhiboyemian')}}</div>
|
||||
<div class="right">
|
||||
<img src="@/static/images/liveshare.png" alt="" @click="fenxiang()" />
|
||||
<img src="@/static/images/liveend.png" alt="" @click="visible = true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<LiveingWatcher :list="roominfo.studentlist" @cameta="sendtext" @vol="senvol"></LiveingWatcher>
|
||||
<div class="LivePlaying">
|
||||
<LivePlaying></LivePlaying>
|
||||
<div class="comment">
|
||||
<div class="commentitem" v-for="(item,index) in imlist" :key="index">
|
||||
<span>{{item.time}}</span>
|
||||
<span class="name"> {{item.name}} : </span>
|
||||
<span> {{item.text}} </span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="others">
|
||||
<div class="othersitem" v-for="(item, index) in roominfo.studentlist" :key="index">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">{{item.name}}</div>
|
||||
<div class="sbox" :id="'s-' + item.memberid"></div>
|
||||
<!-- <img src="" alt="" /> -->
|
||||
</div>
|
||||
<!-- <div class="othersitem">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">asdsada</div>
|
||||
<img src="" alt="" />
|
||||
</div>
|
||||
<div class="othersitem">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">asdsada</div>
|
||||
<img src="" alt="" />
|
||||
</div>
|
||||
<div class="othersitem">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">asdsada</div>
|
||||
<img src="" alt="" />
|
||||
</div> -->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a-modal v-model:visible="visible" title="Basic Modal" @ok="guanbi">
|
||||
<p>{{lan.$t('querenguanbi')}}</p>
|
||||
</a-modal>
|
||||
<a-modal v-model:visible="xuanze" title="提示" okText="摄像头" cancelText="屏幕分享" @ok="xianze(1)" @cancel="xianze(0)" :closable="false" :maskClosable="false">
|
||||
<p>请选择开播方式</p>
|
||||
</a-modal>
|
||||
</div>
|
||||
<iframe src="./zhibo.html" frameborder="0"></iframe>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.liveing ::v-deep(.ant-breadcrumb) > span:last-child {
|
||||
color: #08ae98;
|
||||
}
|
||||
.liveing {
|
||||
width: 1320px;
|
||||
height: 563px;
|
||||
.top {
|
||||
width: 1321px;
|
||||
height: 57px;
|
||||
background: white;
|
||||
border-radius: 18px;
|
||||
margin-top: 23px;
|
||||
margin-bottom: 29px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #121212;
|
||||
font-size: 13px;
|
||||
align-items: center;
|
||||
.left {
|
||||
margin-left: 40px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.right {
|
||||
margin-right: 27px;
|
||||
> img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.LivePlaying {
|
||||
border-radius: 18px;
|
||||
margin: 0 35px;
|
||||
.comment {
|
||||
width: 797px;
|
||||
height: 153px;
|
||||
background: white;
|
||||
border-radius: 18px;
|
||||
margin-top: 29px;
|
||||
padding: 30px;
|
||||
font-size: 12px;
|
||||
overflow-y: auto;
|
||||
color: #121212;
|
||||
|
||||
.commentitem {
|
||||
margin-bottom: 17px;
|
||||
.name {
|
||||
margin-left: 28px;
|
||||
color: #08ae98;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.liveinfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 58px;
|
||||
align-items: center;
|
||||
.left {
|
||||
display: flex;
|
||||
color: #121212;
|
||||
font-size: 13px;
|
||||
margin-left: 29px;
|
||||
> div {
|
||||
margin-right: 57px;
|
||||
}
|
||||
.icon {
|
||||
width: 25px;
|
||||
height: 24px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
width: 74px;
|
||||
height: 29px;
|
||||
border: 1px solid #08ae98;
|
||||
border-radius: 3px;
|
||||
margin-right: 29px;
|
||||
color: #08ae98;
|
||||
font-size: 13px;
|
||||
line-height: 29px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.others {
|
||||
width: 252px;
|
||||
height: 630px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.othersitem {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 132px;
|
||||
margin-bottom: 35px;
|
||||
background-color: #eee;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
overflow: hidden;
|
||||
.watcher {
|
||||
width: 100%;
|
||||
height: 29px;
|
||||
background: #000000;
|
||||
color: white;
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
top: 0;
|
||||
}
|
||||
> img {
|
||||
width: 228px;
|
||||
height: 132px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
.name {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 28px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
.sbox{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, resolveComponent } from "vue";
|
||||
import LivePlaying from "@/components/LivePlaying.vue";
|
||||
import LiveingWatcher from "@/components/LiveingWatcher.vue";
|
||||
import TRTC from "trtc-js-sdk"
|
||||
import { getliveinfo, livestop, luzhi, usersig } from '@/api';
|
||||
import { useRoute } from 'vue-router';
|
||||
import store from '@/store';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useI18n } from '@/utils/i18n';
|
||||
import TIM from 'tim-js-sdk';
|
||||
import dayjs from 'dayjs';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
LivePlaying,
|
||||
LiveingWatcher,
|
||||
},
|
||||
setup() {
|
||||
const lan: any = useI18n();
|
||||
console.log(1);
|
||||
let client: any;
|
||||
let localStream: any;
|
||||
let statie = true;
|
||||
let userSing = '';
|
||||
let type = false;
|
||||
const visible = ref(false);
|
||||
const roominfo = ref<any>([]);
|
||||
console.log(useRoute())
|
||||
const id = useRoute().query.id;
|
||||
let tim: any;
|
||||
TRTC.checkSystemRequirements().then((result: any) => {
|
||||
if(!result) {
|
||||
message.error(lan.$t('buzhichitonghua'))
|
||||
}
|
||||
})
|
||||
async function qiehuan(){
|
||||
// 1 屏幕分享 2 摄像头
|
||||
client.unpublish(localStream)
|
||||
localStream = type ? TRTC.createStream({ userid: store.state.userinfo.memberid, audio: true, screen: true }) : TRTC.createStream({ userId: 10, audio: false, video: true });
|
||||
type = !type;
|
||||
localStream.initialize().then(()=>{
|
||||
client
|
||||
.publish(localStream)
|
||||
.catch((error: string) => {
|
||||
console.error('本地流发布失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
localStream.play('local_stream');
|
||||
console.log('本地流发布成功');
|
||||
});
|
||||
});
|
||||
setup(){
|
||||
return {
|
||||
|
||||
}
|
||||
async function shexiang(){
|
||||
localStream = TRTC.createStream({ userId: store.state.userinfo.memberid, audio: true, video: true });
|
||||
const id = localStream.getId();
|
||||
await localStream
|
||||
.initialize()
|
||||
.catch((error: string) => {
|
||||
console.error('初始化本地流失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('初始化本地流成功');
|
||||
client
|
||||
.publish(localStream)
|
||||
.catch((error: string) => {
|
||||
console.error('本地流发布失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
localStream.play('local_stream');
|
||||
console.log('本地流发布成功');
|
||||
console.log(id, 123)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function pingmu(){
|
||||
const result = await TRTC.checkSystemRequirements()
|
||||
console.log(result,11111)
|
||||
if(!result) {
|
||||
message.error(lan.$t('buzhichifenxiang'));
|
||||
shexiang()
|
||||
return ;
|
||||
}
|
||||
|
||||
localStream = TRTC.createStream({ userid: store.state.userinfo.memberid, audio: true, screen: true });
|
||||
const id = localStream.getId();
|
||||
await localStream
|
||||
.initialize()
|
||||
.catch((error: string) => {
|
||||
console.error('初始化本地流失败 ' + error);
|
||||
message.error(lan.$t('xuanzefenxiangneirong'))
|
||||
|
||||
setTimeout(()=>{
|
||||
pingmu()
|
||||
}, 1000)
|
||||
})
|
||||
.then(() => {
|
||||
console.log('初始化本地流成功');
|
||||
client
|
||||
.publish(localStream)
|
||||
.catch((error: string) => {
|
||||
console.log('本地流发布失败 ' + error);
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
localStream.play('local_stream');
|
||||
console.log('本地流发布成功');
|
||||
console.log(id , 123)
|
||||
});
|
||||
});
|
||||
}
|
||||
const imlist = ref<any>([])
|
||||
async function init(fun: any, userSig: string): Promise<void>{
|
||||
console.log(userSig)
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
if (typeof id == "string") {
|
||||
roominfo.value = await getliveinfo(parseInt(id))
|
||||
console.log(roominfo.value)
|
||||
}
|
||||
|
||||
|
||||
client = TRTC.createClient({
|
||||
mode: 'rtc',
|
||||
sdkAppId: '1400435767',
|
||||
userId: store.state.userinfo.memberid,
|
||||
userSig: userSig
|
||||
});
|
||||
// 监听远端开启推流
|
||||
client.on('stream-added', (event: { stream: any }) => {
|
||||
const remoteStream = event.stream;
|
||||
console.log('远端流增加: ' + remoteStream.getId());
|
||||
//订阅远端流
|
||||
client.subscribe(remoteStream);
|
||||
});
|
||||
// 远端流初始化成功 本地播放
|
||||
client.on('stream-subscribed', (event: { stream: any }) => {
|
||||
const remoteStream = event.stream;
|
||||
console.log(remoteStream);
|
||||
// 播放远端流
|
||||
const el = document.querySelector('#s-' + remoteStream.userId_);
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
remoteStream.play('s-' + remoteStream.userId_);
|
||||
});
|
||||
// 远端关闭麦克风
|
||||
client.on('mute-audio', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端关闭麦克风')
|
||||
});
|
||||
// 远端关闭摄像头
|
||||
client.on('mute-video', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端关闭摄像头')
|
||||
|
||||
});
|
||||
// 远端打开麦克风
|
||||
client.on('unmute-audio', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端打开麦克风')
|
||||
|
||||
});
|
||||
// 远端打开摄像头
|
||||
client.on('unmute-video', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端打开摄像头')
|
||||
|
||||
});
|
||||
client
|
||||
.join({ roomId: roominfo.value.roomid})
|
||||
.catch((error: string) => {
|
||||
console.error('进房失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('进房成功');
|
||||
// if(typeof id == "string"){
|
||||
luzhi(roominfo.value.roomid)
|
||||
// }
|
||||
fun()
|
||||
});
|
||||
|
||||
// im 初始化
|
||||
tim = TIM.create({
|
||||
SDKAppID: 1400435767
|
||||
}); // SDK 实例通常用 tim 表示
|
||||
tim.setLogLevel(0);
|
||||
tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event: any) {
|
||||
// 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
|
||||
// event.name - TIM.EVENT.MESSAGE_RECEIVED
|
||||
// event.data - 存储 Message 对象的数组 - [Message]
|
||||
for(const i in event.data){
|
||||
console.log(event.data[i])
|
||||
const now = dayjs(event.data[i].time)
|
||||
if(!event.data[i].payload.text){
|
||||
break;
|
||||
}
|
||||
imlist.value.push({
|
||||
name: event.data[i].nick,
|
||||
text: event.data[i].payload.text,
|
||||
time: `${now.hour()}:${now.minute()}:${now.second()}`
|
||||
})
|
||||
const div = document.querySelector(".comment")
|
||||
if(div){
|
||||
div.scrollTop = div.scrollHeight
|
||||
}
|
||||
}
|
||||
});
|
||||
tim.on(TIM.EVENT.GROUP_LIST_UPDATED, function(event: any) {
|
||||
// 收到群组列表更新通知,可通过遍历 event.data 获取群组列表数据并渲染到页面
|
||||
// event.name - TIM.EVENT.GROUP_LIST_UPDATED
|
||||
// event.data - 存储 Group 对象的数组 - [Group]
|
||||
console.log(event.data)
|
||||
});
|
||||
tim.login({userID: store.state.userinfo.memberid.toString(), userSig: userSig}).then((res: any)=>{
|
||||
console.log(res.data); // 登录成功
|
||||
if (res.data.repeatLogin === true) {
|
||||
// 标识账号已登录,本次登录操作为重复登录。v2.5.1 起支持
|
||||
console.log(res.data.errorInfo);
|
||||
}
|
||||
|
||||
}).catch(function(imError: any) {
|
||||
console.warn('login error:', imError); // 登录失败的相关信息
|
||||
});
|
||||
tim.on(TIM.EVENT.SDK_READY, function (){
|
||||
const promise = tim.createGroup({
|
||||
type: TIM.TYPES.GRP_AVCHATROOM,
|
||||
name: 'live',
|
||||
groupID: roominfo.value.roomid
|
||||
});
|
||||
promise.then(function(imResponse: any) { // 创建成功
|
||||
console.log(imResponse.data.group); // 创建的群的资料
|
||||
tim.joinGroup({
|
||||
groupID: roominfo.value.roomid,
|
||||
type: TIM.TYPES.GRP_AVCHATROOM
|
||||
}).then((res: any)=>{
|
||||
switch (res.data.status) {
|
||||
case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: // 等待管理员同意
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功
|
||||
console.log(res.data.group); // 加入的群组资料
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // 已经在群中
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}).catch((err: any)=>{
|
||||
console.log(err)
|
||||
})
|
||||
}).catch(function(imError: any) {
|
||||
console.warn('createGroup error:', imError); // 创建群组失败的相关信息
|
||||
tim.joinGroup({
|
||||
groupID: roominfo.value.roomid,
|
||||
type: TIM.TYPES.GRP_AVCHATROOM
|
||||
}).then((res: any)=>{
|
||||
switch (res.data.status) {
|
||||
case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: // 等待管理员同意
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功
|
||||
console.log(res.data.group); // 加入的群组资料
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // 已经在群中
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}).catch((err: any)=>{
|
||||
console.log(err)
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
async function fenxiang(){
|
||||
console.log(localStream)
|
||||
|
||||
|
||||
await client.unpublish(localStream).then(() => {
|
||||
// 关闭屏幕分享流
|
||||
console.log("关闭")
|
||||
// client.leave().then(() => {
|
||||
// // leaving room success
|
||||
// console.log("关闭成功")
|
||||
// }).catch((error: string) => {
|
||||
// console.error('leaving room failed: ' + error);
|
||||
// });
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
});
|
||||
statie ? await shexiang() : await pingmu();
|
||||
statie = !statie;
|
||||
console.log(localStream.getId())
|
||||
}
|
||||
|
||||
function guanbi(){
|
||||
client.leave().then(() => {
|
||||
// leaving room success
|
||||
visible.value = false;
|
||||
if(typeof id == "string"){
|
||||
livestop(id, roominfo.value.roomid)
|
||||
}
|
||||
}).catch((error: string) => {
|
||||
message.error(lan.$t('guanbishibai')+':' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function sendtext(id: number){
|
||||
const m = tim.createTextMessage({
|
||||
to: roominfo.value.roomid,
|
||||
conversationType: TIM.TYPES.CONV_GROUP,
|
||||
payload: {
|
||||
text: `beelinkMuteUserId:${id},isClose:0`
|
||||
}
|
||||
});
|
||||
const promise = tim.sendMessage(m);
|
||||
promise.then(function(imResponse: any) {
|
||||
// 发送成功
|
||||
console.log(imResponse);
|
||||
message.success("发送命令成功")
|
||||
}).catch(function(imError: any) {
|
||||
// 发送失败
|
||||
message.error("发送命令失败")
|
||||
console.warn('sendMessage error:', imError);
|
||||
});
|
||||
}
|
||||
function sendvol(id: number){
|
||||
const m = tim.createTextMessage({
|
||||
to: roominfo.value.roomid,
|
||||
conversationType: TIM.TYPES.CONV_GROUP,
|
||||
payload: {
|
||||
text: `beelinkTurnOffTheCameraUserId:${id},isClose:1
|
||||
0`
|
||||
}
|
||||
});
|
||||
const promise = tim.sendMessage(m);
|
||||
promise.then(function(imResponse: any) {
|
||||
// 发送成功
|
||||
console.log(imResponse);
|
||||
message.success("发送命令成功")
|
||||
}).catch(function(imError: any) {
|
||||
// 发送失败
|
||||
message.error("发送命令失败")
|
||||
console.warn('sendMessage error:', imError);
|
||||
});
|
||||
}
|
||||
const xuanze = ref(true)
|
||||
async function xianze(index: number){
|
||||
if(store.state.userinfo.memberid != 0 && store.state.userinfo.memberid){
|
||||
// clearInterval(si);
|
||||
userSing = await usersig(store.state.userinfo.memberid);
|
||||
init(index == 0 ? pingmu : shexiang, userSing);
|
||||
xuanze.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// onMounted(async ()=>{
|
||||
// const si = setInterval(async ()=>{
|
||||
|
||||
// })
|
||||
|
||||
// })
|
||||
|
||||
|
||||
|
||||
return{
|
||||
fenxiang,
|
||||
qiehuan,
|
||||
roominfo,
|
||||
guanbi,
|
||||
visible,
|
||||
lan,
|
||||
sendtext,
|
||||
imlist,
|
||||
xianze,
|
||||
xuanze
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
587
src/views/regime/LiveingBACK.vue
Normal file
@ -0,0 +1,587 @@
|
||||
<template>
|
||||
<div class="liveing">
|
||||
|
||||
<div class="top">
|
||||
<div class="left">{{lan.$t('zhiboyemian')}}</div>
|
||||
<div class="right">
|
||||
<img src="@/static/images/liveshare.png" alt="" @click="fenxiang()" />
|
||||
<img src="@/static/images/liveend.png" alt="" @click="visible = true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<LiveingWatcher :list="roominfo.studentlist" @cameta="sendtext" @vol="senvol"></LiveingWatcher>
|
||||
<div class="LivePlaying">
|
||||
<LivePlaying></LivePlaying>
|
||||
<div class="comment">
|
||||
<div class="commentitem" v-for="(item,index) in imlist" :key="index">
|
||||
<span>{{item.time}}</span>
|
||||
<span class="name"> {{item.name}} : </span>
|
||||
<span> {{item.text}} </span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="others">
|
||||
<div class="othersitem" v-for="(item, index) in roominfo.studentlist" :key="index">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">{{item.name}}</div>
|
||||
<div class="sbox" :id="'s-' + item.memberid"></div>
|
||||
<!-- <img src="" alt="" /> -->
|
||||
</div>
|
||||
<!-- <div class="othersitem">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">asdsada</div>
|
||||
<img src="" alt="" />
|
||||
</div>
|
||||
<div class="othersitem">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">asdsada</div>
|
||||
<img src="" alt="" />
|
||||
</div>
|
||||
<div class="othersitem">
|
||||
<div class="watcher"></div>
|
||||
<div class="name">asdsada</div>
|
||||
<img src="" alt="" />
|
||||
</div> -->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a-modal v-model:visible="visible" title="Basic Modal" @ok="guanbi">
|
||||
<p>{{lan.$t('querenguanbi')}}</p>
|
||||
</a-modal>
|
||||
<a-modal v-model:visible="xuanze" title="提示" okText="摄像头" cancelText="屏幕分享" @ok="xianze(1)" @cancel="xianze(0)" :closable="false" :maskClosable="false">
|
||||
<p>请选择开播方式</p>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.liveing ::v-deep(.ant-breadcrumb) > span:last-child {
|
||||
color: #08ae98;
|
||||
}
|
||||
.liveing {
|
||||
width: 1320px;
|
||||
height: 563px;
|
||||
.top {
|
||||
width: 1321px;
|
||||
height: 57px;
|
||||
background: white;
|
||||
border-radius: 18px;
|
||||
margin-top: 23px;
|
||||
margin-bottom: 29px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #121212;
|
||||
font-size: 13px;
|
||||
align-items: center;
|
||||
.left {
|
||||
margin-left: 40px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.right {
|
||||
margin-right: 27px;
|
||||
> img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.LivePlaying {
|
||||
border-radius: 18px;
|
||||
margin: 0 35px;
|
||||
.comment {
|
||||
width: 797px;
|
||||
height: 153px;
|
||||
background: white;
|
||||
border-radius: 18px;
|
||||
margin-top: 29px;
|
||||
padding: 30px;
|
||||
font-size: 12px;
|
||||
overflow-y: auto;
|
||||
color: #121212;
|
||||
|
||||
.commentitem {
|
||||
margin-bottom: 17px;
|
||||
.name {
|
||||
margin-left: 28px;
|
||||
color: #08ae98;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.liveinfo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 58px;
|
||||
align-items: center;
|
||||
.left {
|
||||
display: flex;
|
||||
color: #121212;
|
||||
font-size: 13px;
|
||||
margin-left: 29px;
|
||||
> div {
|
||||
margin-right: 57px;
|
||||
}
|
||||
.icon {
|
||||
width: 25px;
|
||||
height: 24px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
width: 74px;
|
||||
height: 29px;
|
||||
border: 1px solid #08ae98;
|
||||
border-radius: 3px;
|
||||
margin-right: 29px;
|
||||
color: #08ae98;
|
||||
font-size: 13px;
|
||||
line-height: 29px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.others {
|
||||
width: 252px;
|
||||
height: 630px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.othersitem {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 132px;
|
||||
margin-bottom: 35px;
|
||||
background-color: #eee;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
overflow: hidden;
|
||||
.watcher {
|
||||
width: 100%;
|
||||
height: 29px;
|
||||
background: #000000;
|
||||
color: white;
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
border-radius: 18px 18px 0px 0px;
|
||||
top: 0;
|
||||
}
|
||||
> img {
|
||||
width: 228px;
|
||||
height: 132px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
.name {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 28px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
.sbox{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref, resolveComponent } from "vue";
|
||||
import LivePlaying from "@/components/LivePlaying.vue";
|
||||
import LiveingWatcher from "@/components/LiveingWatcher.vue";
|
||||
import TRTC from "trtc-js-sdk"
|
||||
import { getliveinfo, livestop, luzhi, usersig } from '@/api';
|
||||
import { useRoute } from 'vue-router';
|
||||
import store from '@/store';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useI18n } from '@/utils/i18n';
|
||||
import TIM from 'tim-js-sdk';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
LivePlaying,
|
||||
LiveingWatcher,
|
||||
},
|
||||
setup() {
|
||||
const lan: any = useI18n();
|
||||
console.log(1);
|
||||
let client: any;
|
||||
let localStream: any;
|
||||
let statie = true;
|
||||
let userSing = '';
|
||||
let type = false;
|
||||
const visible = ref(false);
|
||||
const roominfo = ref<any>([]);
|
||||
console.log(useRoute())
|
||||
const id = useRoute().query.id;
|
||||
let tim: any;
|
||||
TRTC.checkSystemRequirements().then((result: any) => {
|
||||
if(!result) {
|
||||
message.error(lan.$t('buzhichitonghua'))
|
||||
}
|
||||
})
|
||||
async function qiehuan(){
|
||||
// 1 屏幕分享 2 摄像头
|
||||
client.unpublish(localStream)
|
||||
localStream = type ? TRTC.createStream({ userid: store.state.userinfo.memberid, audio: true, screen: true }) : TRTC.createStream({ userId: 10, audio: false, video: true });
|
||||
type = !type;
|
||||
localStream.initialize().then(()=>{
|
||||
client
|
||||
.publish(localStream)
|
||||
.catch((error: string) => {
|
||||
console.error('本地流发布失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
localStream.play('local_stream');
|
||||
console.log('本地流发布成功');
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
async function shexiang(){
|
||||
localStream = TRTC.createStream({ userId: store.state.userinfo.memberid, audio: true, video: true });
|
||||
const id = localStream.getId();
|
||||
await localStream
|
||||
.initialize()
|
||||
.catch((error: string) => {
|
||||
console.error('初始化本地流失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('初始化本地流成功');
|
||||
client
|
||||
.publish(localStream)
|
||||
.catch((error: string) => {
|
||||
console.error('本地流发布失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
localStream.play('local_stream');
|
||||
console.log('本地流发布成功');
|
||||
console.log(id, 123)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function pingmu(){
|
||||
const result = await TRTC.checkSystemRequirements()
|
||||
console.log(result,11111)
|
||||
if(!result) {
|
||||
message.error(lan.$t('buzhichifenxiang'));
|
||||
shexiang()
|
||||
return ;
|
||||
}
|
||||
|
||||
localStream = TRTC.createStream({ userid: store.state.userinfo.memberid, audio: true, screen: true });
|
||||
const id = localStream.getId();
|
||||
await localStream
|
||||
.initialize()
|
||||
.catch((error: string) => {
|
||||
console.error('初始化本地流失败 ' + error);
|
||||
message.error(lan.$t('xuanzefenxiangneirong'))
|
||||
|
||||
setTimeout(()=>{
|
||||
pingmu()
|
||||
}, 1000)
|
||||
})
|
||||
.then(() => {
|
||||
console.log('初始化本地流成功');
|
||||
client
|
||||
.publish(localStream)
|
||||
.catch((error: string) => {
|
||||
console.log('本地流发布失败 ' + error);
|
||||
|
||||
})
|
||||
.then(() => {
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
localStream.play('local_stream');
|
||||
console.log('本地流发布成功');
|
||||
console.log(id , 123)
|
||||
});
|
||||
});
|
||||
}
|
||||
const imlist = ref<any>([])
|
||||
async function init(fun: any, userSig: string): Promise<void>{
|
||||
console.log(userSig)
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
if (typeof id == "string") {
|
||||
roominfo.value = await getliveinfo(parseInt(id))
|
||||
console.log(roominfo.value)
|
||||
}
|
||||
|
||||
|
||||
client = TRTC.createClient({
|
||||
mode: 'rtc',
|
||||
sdkAppId: '1400435767',
|
||||
userId: store.state.userinfo.memberid,
|
||||
userSig: userSig
|
||||
});
|
||||
// 监听远端开启推流
|
||||
client.on('stream-added', (event: { stream: any }) => {
|
||||
const remoteStream = event.stream;
|
||||
console.log('远端流增加: ' + remoteStream.getId());
|
||||
//订阅远端流
|
||||
client.subscribe(remoteStream);
|
||||
});
|
||||
// 远端流初始化成功 本地播放
|
||||
client.on('stream-subscribed', (event: { stream: any }) => {
|
||||
const remoteStream = event.stream;
|
||||
console.log(remoteStream);
|
||||
// 播放远端流
|
||||
const el = document.querySelector('#s-' + remoteStream.userId_);
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
remoteStream.play('s-' + remoteStream.userId_);
|
||||
});
|
||||
// 远端关闭麦克风
|
||||
client.on('mute-audio', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端关闭麦克风')
|
||||
});
|
||||
// 远端关闭摄像头
|
||||
client.on('mute-video', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端关闭摄像头')
|
||||
|
||||
});
|
||||
// 远端打开麦克风
|
||||
client.on('unmute-audio', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端打开麦克风')
|
||||
|
||||
});
|
||||
// 远端打开摄像头
|
||||
client.on('unmute-video', (event: any) => {
|
||||
const userId = event.userId;
|
||||
console.log(userId, '远端打开摄像头')
|
||||
|
||||
});
|
||||
client
|
||||
.join({ roomId: roominfo.value.roomid})
|
||||
.catch((error: string) => {
|
||||
console.error('进房失败 ' + error);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('进房成功');
|
||||
// if(typeof id == "string"){
|
||||
luzhi(roominfo.value.roomid)
|
||||
// }
|
||||
fun()
|
||||
});
|
||||
|
||||
// im 初始化
|
||||
tim = TIM.create({
|
||||
SDKAppID: 1400435767
|
||||
}); // SDK 实例通常用 tim 表示
|
||||
tim.setLogLevel(0);
|
||||
tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event: any) {
|
||||
// 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面
|
||||
// event.name - TIM.EVENT.MESSAGE_RECEIVED
|
||||
// event.data - 存储 Message 对象的数组 - [Message]
|
||||
for(const i in event.data){
|
||||
console.log(event.data[i])
|
||||
const now = dayjs(event.data[i].time)
|
||||
if(!event.data[i].payload.text){
|
||||
break;
|
||||
}
|
||||
imlist.value.push({
|
||||
name: event.data[i].nick,
|
||||
text: event.data[i].payload.text,
|
||||
time: `${now.hour()}:${now.minute()}:${now.second()}`
|
||||
})
|
||||
const div = document.querySelector(".comment")
|
||||
if(div){
|
||||
div.scrollTop = div.scrollHeight
|
||||
}
|
||||
}
|
||||
});
|
||||
tim.on(TIM.EVENT.GROUP_LIST_UPDATED, function(event: any) {
|
||||
// 收到群组列表更新通知,可通过遍历 event.data 获取群组列表数据并渲染到页面
|
||||
// event.name - TIM.EVENT.GROUP_LIST_UPDATED
|
||||
// event.data - 存储 Group 对象的数组 - [Group]
|
||||
console.log(event.data)
|
||||
});
|
||||
tim.login({userID: store.state.userinfo.memberid.toString(), userSig: userSig}).then((res: any)=>{
|
||||
console.log(res.data); // 登录成功
|
||||
if (res.data.repeatLogin === true) {
|
||||
// 标识账号已登录,本次登录操作为重复登录。v2.5.1 起支持
|
||||
console.log(res.data.errorInfo);
|
||||
}
|
||||
|
||||
}).catch(function(imError: any) {
|
||||
console.warn('login error:', imError); // 登录失败的相关信息
|
||||
});
|
||||
tim.on(TIM.EVENT.SDK_READY, function (){
|
||||
const promise = tim.createGroup({
|
||||
type: TIM.TYPES.GRP_AVCHATROOM,
|
||||
name: 'live',
|
||||
groupID: roominfo.value.roomid
|
||||
});
|
||||
promise.then(function(imResponse: any) { // 创建成功
|
||||
console.log(imResponse.data.group); // 创建的群的资料
|
||||
tim.joinGroup({
|
||||
groupID: roominfo.value.roomid,
|
||||
type: TIM.TYPES.GRP_AVCHATROOM
|
||||
}).then((res: any)=>{
|
||||
switch (res.data.status) {
|
||||
case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: // 等待管理员同意
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功
|
||||
console.log(res.data.group); // 加入的群组资料
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // 已经在群中
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}).catch((err: any)=>{
|
||||
console.log(err)
|
||||
})
|
||||
}).catch(function(imError: any) {
|
||||
console.warn('createGroup error:', imError); // 创建群组失败的相关信息
|
||||
tim.joinGroup({
|
||||
groupID: roominfo.value.roomid,
|
||||
type: TIM.TYPES.GRP_AVCHATROOM
|
||||
}).then((res: any)=>{
|
||||
switch (res.data.status) {
|
||||
case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: // 等待管理员同意
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功
|
||||
console.log(res.data.group); // 加入的群组资料
|
||||
break;
|
||||
case TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // 已经在群中
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}).catch((err: any)=>{
|
||||
console.log(err)
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
async function fenxiang(){
|
||||
console.log(localStream)
|
||||
|
||||
|
||||
await client.unpublish(localStream).then(() => {
|
||||
// 关闭屏幕分享流
|
||||
console.log("关闭")
|
||||
// client.leave().then(() => {
|
||||
// // leaving room success
|
||||
// console.log("关闭成功")
|
||||
// }).catch((error: string) => {
|
||||
// console.error('leaving room failed: ' + error);
|
||||
// });
|
||||
const el = document.querySelector("#local_stream");
|
||||
if(el){
|
||||
el.innerHTML = ""
|
||||
}
|
||||
});
|
||||
statie ? await shexiang() : await pingmu();
|
||||
statie = !statie;
|
||||
console.log(localStream.getId())
|
||||
}
|
||||
|
||||
function guanbi(){
|
||||
client.leave().then(() => {
|
||||
// leaving room success
|
||||
visible.value = false;
|
||||
if(typeof id == "string"){
|
||||
livestop(id, roominfo.value.roomid)
|
||||
}
|
||||
}).catch((error: string) => {
|
||||
message.error(lan.$t('guanbishibai')+':' + error);
|
||||
});
|
||||
}
|
||||
|
||||
function sendtext(id: number){
|
||||
const m = tim.createTextMessage({
|
||||
to: roominfo.value.roomid,
|
||||
conversationType: TIM.TYPES.CONV_GROUP,
|
||||
payload: {
|
||||
text: `beelinkMuteUserId:${id},isClose:0`
|
||||
}
|
||||
});
|
||||
const promise = tim.sendMessage(m);
|
||||
promise.then(function(imResponse: any) {
|
||||
// 发送成功
|
||||
console.log(imResponse);
|
||||
message.success("发送命令成功")
|
||||
}).catch(function(imError: any) {
|
||||
// 发送失败
|
||||
message.error("发送命令失败")
|
||||
console.warn('sendMessage error:', imError);
|
||||
});
|
||||
}
|
||||
function sendvol(id: number){
|
||||
const m = tim.createTextMessage({
|
||||
to: roominfo.value.roomid,
|
||||
conversationType: TIM.TYPES.CONV_GROUP,
|
||||
payload: {
|
||||
text: `beelinkTurnOffTheCameraUserId:${id},isClose:1
|
||||
0`
|
||||
}
|
||||
});
|
||||
const promise = tim.sendMessage(m);
|
||||
promise.then(function(imResponse: any) {
|
||||
// 发送成功
|
||||
console.log(imResponse);
|
||||
message.success("发送命令成功")
|
||||
}).catch(function(imError: any) {
|
||||
// 发送失败
|
||||
message.error("发送命令失败")
|
||||
console.warn('sendMessage error:', imError);
|
||||
});
|
||||
}
|
||||
const xuanze = ref(true)
|
||||
async function xianze(index: number){
|
||||
if(store.state.userinfo.memberid != 0 && store.state.userinfo.memberid){
|
||||
// clearInterval(si);
|
||||
userSing = await usersig(store.state.userinfo.memberid);
|
||||
init(index == 0 ? pingmu : shexiang, userSing);
|
||||
xuanze.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// onMounted(async ()=>{
|
||||
// const si = setInterval(async ()=>{
|
||||
|
||||
// })
|
||||
|
||||
// })
|
||||
|
||||
|
||||
|
||||
return{
|
||||
fenxiang,
|
||||
qiehuan,
|
||||
roominfo,
|
||||
guanbi,
|
||||
visible,
|
||||
lan,
|
||||
sendtext,
|
||||
imlist,
|
||||
xianze,
|
||||
xuanze
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|