rtc.vue 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  1. <template>
  2. <div align="center" class="rtc-page cover-bg" :style="{height: pageData.height + 'px'}">
  3. <!-- video 容器 -->
  4. <div class="video-wrapper" id="videoRef" :style="{width: pageData.videoWidth + 'px', height: pageData.videoHeight + 'px'}"></div>
  5. <!-- 三menu键 -->
  6. <div id="foot-menu-wrap" :style="`height: ${pageData.footMenuHeight}px`">
  7. <div @click.stop="sendKey(187)"><van-icon name="wap-nav" size="24px"/></div>
  8. <div @click.stop="sendKey(3)"><van-icon name="wap-home-o" size="24px"/></div>
  9. <div @click.stop="sendKey(4)"><van-icon name="arrow-left" size="24px"/></div>
  10. </div>
  11. <!-- 悬浮按钮 -->
  12. <FloatBtn :width="pageData.width" :height="pageData.height" :latency="rtcNetwork.currentRoundTripTime" @onClick="levitatedSphereVisible = true"/>
  13. <!-- 左侧popup -->
  14. <LeftMenuPopup
  15. ref="leftMenuPopupRef"
  16. :engine="engine"
  17. :userCardId="this.parametersData.userCardId"
  18. :levitatedSphereVisible.sync="levitatedSphereVisible"
  19. :latency="rtcNetwork.currentRoundTripTime"
  20. :groupList="groupList"
  21. :cloudList="cloudList"
  22. :mealTypeObj="mealTypeObj"
  23. @funcHandle="funcHandle"
  24. @exit="exit"
  25. />
  26. <!-- 右侧popup -->
  27. <!-- <RightPopup ref="rightPopupRef" :engine="engine" :userCardId="this.parametersData.userCardId" :levitatedSphereVisible.sync="levitatedSphereVisible" @shearplate="shearplate" @exit="exit"/> -->
  28. <!-- 输入并复制到粘贴板 -->
  29. <InputCopy ref="inputCopyRef" @openPasteboard="openPasteboard"/>
  30. <!-- 云机内部的粘贴板内容 -->
  31. <CloudPhoneClipboard ref="cloudPhoneClipboardRef"/>
  32. <!-- 超时无操作 -->
  33. <TimeoutNoOps ref="timeoutNoOpsRef" />
  34. <!-- 计时卡计时 | 计费规则 | 应用推荐 -->
  35. <TimeBalance ref="timeBalanceRef" :parametersData="parametersData" @downline="$refs.rightPopupRef.downline()"/>
  36. </div>
  37. </template>
  38. <script>
  39. import meta from './config/meta.js';
  40. import request from './config/request.js';
  41. import logReport from './config/logReport.js';
  42. import publicMixin from './mixins/public.js';
  43. import * as uni from '../../static/static/js/uni.webview.1.5.2.js';
  44. import FloatBtn from './components/FloatBtn.vue';
  45. import RightPopup from './components/RightPopup.vue';
  46. import LeftMenuPopup from './components/LeftMenuPopup.vue';
  47. import InputCopy from './components/InputCopy.vue';
  48. import CloudPhoneClipboard from './components/CloudPhoneClipboard.vue';
  49. import TimeoutNoOps from './components/TimeoutNoOps.vue';
  50. import TimeBalance from './components/TimeBalance.vue';
  51. /**
  52. * @description: 判断当前页面运行环境
  53. * @return {Object} 返回当前页面运行环境
  54. */
  55. const isBrowserEnvironment = function() {
  56. // 判断是否在浏览器环境中
  57. const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined';
  58. // 判断是否在微信环境中
  59. const isWechat = /MicroMessenger/i.test(navigator.userAgent);
  60. // 判断是否在微信小程序的 web-view 中
  61. const isMiniProgramWebview = isWechat && /miniProgram/i.test(navigator.userAgent);
  62. // 判断是否是 iPhone 设备
  63. const isIPhone = /iPhone/i.test(navigator.userAgent);
  64. // 判断是否是顶级窗口(不是嵌套在 iframe 中)目前只有ios的浏览器中才会是顶级窗口
  65. const isTopWindow = window.parent === window.self;
  66. return {
  67. isBrowser, // 是否在浏览器环境中
  68. isWechat, // 是否在微信环境中
  69. isMiniProgramWebview, // 是否在微信小程序的 web-view 中
  70. isIPhone, // 是否是 iPhone 设备
  71. isTopWindow, // 是否是顶级窗口
  72. };
  73. }
  74. export default {
  75. auth: false,
  76. name: 'webRTC',
  77. layout: 'cloudPhone',
  78. components: {
  79. FloatBtn,
  80. RightPopup,
  81. InputCopy,
  82. CloudPhoneClipboard,
  83. TimeoutNoOps,
  84. TimeBalance,
  85. LeftMenuPopup,
  86. },
  87. head() {
  88. return {
  89. title: '云手机',
  90. meta: [ ...meta ],
  91. script: [
  92. {
  93. // ./ 路径指向nuxt.config.js同级目录的static文件夹
  94. src: './rtcEngine/config/js/SDK.min.js', // sdk 2.0文件
  95. type: 'text/javascript',
  96. async: false,
  97. defer: false,
  98. onload: '$_script_loadHandler()', // 加载成功回调created生命周期中定义的方法
  99. onerror: '$_script_errHandler()', // 加载失败回调created生命周期中定义的方法
  100. },
  101. // {
  102. // // ./ 路径指向nuxt.config.js同级目录的static文件夹
  103. // src: './static/js/uni.webview.1.5.2.js', // uniapp webview 1.5.2文件
  104. // type: 'text/javascript',
  105. // async: true,
  106. // defer: false,
  107. // onload: '$_script_uni_loadHandler()', // 加载成功回调created生命周期中定义的方法
  108. // }
  109. ]
  110. }
  111. },
  112. mixins: [publicMixin],
  113. data() {
  114. return {
  115. // 日志上报实例
  116. logReportObj: null,
  117. // SDK加载状态
  118. sdkLoadStatus: 'loading', // sdk 加载状态 [loading|success|error]
  119. // 页面数据
  120. pageData: {
  121. width: 0, // 页面宽度
  122. height: 0, // 页面高度
  123. footMenuHeight: 40, // 底部菜单高度
  124. videoWidth: 0, // 视频宽度
  125. videoHeight: 0, // 视频高度
  126. },
  127. // 是否支持webRTC
  128. isSupportRtc: !!(
  129. typeof RTCPeerConnection !== 'undefined' &&
  130. typeof RTCIceCandidate !== 'undefined' &&
  131. typeof RTCSessionDescription !== 'undefined'
  132. ),
  133. // url 问号后的参数
  134. parametersData: {
  135. /**
  136. * @description: 传递的参数
  137. * @param {String} record 数据id
  138. * @param {Number} userCardId 云机的id
  139. * @param {String} mealType 云机套餐类型 eg: VIP、STARBALL...
  140. * @param {Number} sourceType 云机来源: 0:购买 1试用 2:免费激活码 3:免费活动抽奖 4:ar app注册 5:9.9元套餐年卡 ',
  141. * @param {Number} userCardType 云机的类型 0 普通套餐 1、2、3:年卡、普通计时、自动续费普通计时
  142. * @param {Number} validTime 卡的有效期
  143. * @param {String} rm 卡所在的机房
  144. * @param {Number} isShowCountdown 是否显示倒计时 0:否 1:是
  145. * @param {Number} isShowRule 是否显示规则 0:否 1:是
  146. * @param {Number} timingStatus 卡的计时状态 0:停止计时 1:开始计时 2:待确认开始计时"
  147. * @param {String} isFirstConnect 是否是首次连接
  148. * @param {Number} authPhone 0自身购买的云手机 1获取得到的云手机
  149. * @param {String} username 用户名
  150. * @param {String} token token
  151. * @param {Number} isTips 是否显示提示 0:否 1:是
  152. * @param {Number} isWeixin 是否是微信小程序环境 0:否 1:是
  153. * @param {String} merchantSign 商户标识
  154. */
  155. },
  156. // 卡的连接信息
  157. connectData: {},
  158. // 云手机引擎 播放器实例
  159. engine: {},
  160. rtcNetwork: {
  161. currentRoundTripTime: 0, // 当前往返时间(网络延迟)
  162. }, // webRtc网络分析数据
  163. doConnectDirectivesWs: null, // 云手机指令通道
  164. doConnectDirectivesIntervalerPing: null, // 业务通道定时标识 云手机指令通道心跳
  165. doConnectDirectivesRequestNum: 1, // 业务通道重连次数
  166. doConnectDirectivesRequestNumMax: 6, // 业务通道重连次数上限
  167. // 右侧popup显隐
  168. levitatedSphereVisible: false,
  169. }
  170. },
  171. // 页面初始化后触发
  172. async fetch() {
  173. // 获取页面传递参数
  174. this.parametersData = this.$route.query;
  175. // 获取用户所有云机列表
  176. this.getCloudList();
  177. // 获取所有云机套餐信息
  178. this.getMealIconInfo();
  179. // 获取云机分组
  180. this.getCloudGroupId();
  181. },
  182. computed: {
  183. // 是否为微信浏览器环境
  184. isWeChatBrowser() {
  185. return this.$userAgent.isWx;
  186. },
  187. },
  188. created() {
  189. this.$toast.loading({
  190. duration: 0, // 持续展示 toast
  191. message: '数据加载中...',
  192. });
  193. // 设置html标签的背景为黑色
  194. document.body.style.background = '#000';
  195. // 定义全局变量 用于监听sdk加载状态
  196. window.$_script_loadHandler = async ()=> {
  197. console.log('$_script_loadHandler: SDK加载成功');
  198. this.sdkLoadStatus = 'success';
  199. // 初始化日志上报
  200. this.initLogReport();
  201. // 调用接口获取卡连接数据
  202. const cardData = await this.getConnectData(this.parametersData);
  203. // 判断卡的连接方式
  204. const connectData = await this.judgeConnectType(cardData);
  205. // 保存卡连接信息
  206. this.connectData = connectData;
  207. this.initWebRtc();
  208. };
  209. window.$_script_errHandler = ()=> {
  210. console.log('SDK加载失败');
  211. this.sdkLoadStatus = 'error';
  212. }
  213. },
  214. mounted() {
  215. // 获取窗口尺寸
  216. this.getInitSize();
  217. window.onresize = () => {
  218. console.log('窗口尺寸变化');
  219. this.getInitSize()
  220. };
  221. // 初始化页面监听事件
  222. this.initListener();
  223. },
  224. // 页面销毁前触发
  225. beforeDestroy() {
  226. // 销毁页面监听事件
  227. this.destroyListener();
  228. },
  229. methods: {
  230. // 初始化页面监听事件
  231. initListener() {
  232. // 禁止双击缩放
  233. document.addEventListener('dblclick', this.preventDefault);
  234. // 添加监听 页面显示或隐藏 事件
  235. document.addEventListener('visibilitychange', this.visibilitychanged);
  236. },
  237. // 销毁页面监听事件
  238. destroyListener() {
  239. // 允许双击缩放
  240. document.removeEventListener('dblclick', this.preventDefault);
  241. // 移除监听 页面显示或隐藏 事件
  242. document.removeEventListener('visibilitychange', this.visibilitychanged);
  243. },
  244. // 阻止默认事件
  245. preventDefault(e) {
  246. e.preventDefault();
  247. },
  248. // 监听 页面显示或隐藏 执行的函数
  249. visibilitychanged() {
  250. // 获取当前环境
  251. const env = isBrowserEnvironment();
  252. // 获取当前页面的可见性状态
  253. const visibilityState = document.visibilityState;
  254. if (visibilityState === 'visible') {
  255. // 页面显示时的逻辑
  256. // 网页重载
  257. if (env.isBrowser && env.isTopWindow && env.isIPhone) {
  258. location.reload();
  259. }
  260. } else if (visibilityState === 'hidden') {
  261. // 页面隐藏时的逻辑
  262. // video.pause();
  263. } else if (visibilityState === 'prerender') {
  264. // 页面预渲染时的逻辑
  265. console.log('页面处于预渲染状态');
  266. } else if (visibilityState === 'unloaded') {
  267. // 页面即将卸载时的逻辑 移除监听
  268. document.removeEventListener('visibilitychange', this.visibilitychanged);
  269. }
  270. },
  271. // 获取初始化尺寸
  272. getInitSize() {
  273. // 获取窗口尺寸
  274. this.pageData.height = window.innerHeight;
  275. this.pageData.width = window.innerWidth;
  276. // 计算视频尺寸 webRTC需要做成16:9的画面
  277. let videoWidth = this.pageData.width;
  278. let videoHeight = this.pageData.height - this.pageData.footMenuHeight;
  279. // 计算当前视口的宽高比
  280. const currentRatio = videoWidth / videoHeight;
  281. console.log(`当前视口的宽高比: ${currentRatio}`);
  282. // 9:16 的目标比例
  283. const targetRatio = 9 / 16;
  284. // 判断当前视口的宽高比与目标比例的关系
  285. if (currentRatio > targetRatio) {
  286. // 当前视口的宽高比大于目标比例,说明宽度“过宽”,需要以高度为基准
  287. console.log("当前视口宽度过宽,应以高度为基准调整宽度");
  288. this.pageData.videoWidth = videoHeight * targetRatio;
  289. this.pageData.videoHeight = videoHeight;
  290. console.log(`1目标: 宽${this.pageData.videoWidth},高${this.pageData.videoHeight}`);
  291. } else {
  292. // 当前视口的宽高比小于目标比例,说明高度“过高”,需要以宽度为基准
  293. console.log("当前视口高度过高,应以宽度为基准调整高度");
  294. this.pageData.videoWidth = videoWidth;
  295. this.pageData.videoHeight = videoWidth / targetRatio;
  296. console.log(`2目标: 宽${this.pageData.videoWidth},高${this.pageData.videoHeight}`);
  297. }
  298. },
  299. // 获取卡的信息
  300. async getConnectData(params) {
  301. let userCardId = params.userCardId;
  302. try {
  303. // 设置上报参数
  304. this.logReportObj.setParams({userCardId});
  305. let isWx = this.$userAgent.isWx;
  306. let { isWeixin } = params;
  307. let clientType = (+isWeixin || isWx) ? 'wx' : undefined;
  308. // 设置上报参数
  309. this.logReportObj.setParams({userCardId});
  310. clientType && this.logReportObj.setParams({clientType});
  311. const res = await this.$axios.$post('/resources/user/cloud/connect', { userCardId }, {
  312. headers: {
  313. merchantSign: params.merchantSign,
  314. },
  315. });
  316. if (!res.success) {
  317. // 设置日志 推流状态为失败,和链接状态
  318. this.logReportObj.setParams({plugFowStatus: 2, linkWay: this.logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: this.logReportObj.formatDate(new Date())});
  319. // 日志上报
  320. this.logReportObj.collectLog(
  321. `接口获取数据失败:
  322. url: /api/resources/user/cloud/connect
  323. method: post
  324. 参数: ${JSON.stringify({ userCardId })}
  325. 响应: ${JSON.stringify(res)}`
  326. );
  327. // res.status状态码枚举值 0: 正常
  328. const statusEnum = {
  329. // 5200:RBD资源挂载中
  330. 5200: '网络异常,请稍后重试',
  331. // 入使用排队9.9,年卡
  332. 5220: '云手机正在一键修复中',
  333. 5203: '正在排队中,请稍等',
  334. // 9.9年卡连接异常,重新进入排队
  335. 5204: '云机异常,正在为你重新分配云机',
  336. 5228: '卡的网络状态为差',
  337. 5229: '接口返回链接信息缺失',
  338. };
  339. // NOTE 这里可设置重连机制, 重连次数上限6次, 每次重连间隔3秒, 暂不做重连
  340. // 提示错误信息
  341. this.$toast(statusEnum[res.status] || '网络异常,请稍后重试');
  342. return Promise.reject(new Error(statusEnum[res.status] || '网络异常,请稍后重试'));
  343. }
  344. return res.data;
  345. }catch (error) {
  346. // 设置上报参数
  347. this.logReportObj.setParams({linkWay: 4, plugFowStatus: 2, linkEndTime: this.logReportObj.formatDate(new Date())});
  348. // 日志上报
  349. this.logReportObj.collectLog(
  350. `接口获取数据失败:
  351. url: /api/resources/user/cloud/connect
  352. method: post
  353. 参数: ${JSON.stringify({ userCardId })}
  354. 响应: ${JSON.stringify(error)}`
  355. );
  356. console.log('error connectAxios:', error);
  357. return Promise.reject(error);
  358. }
  359. },
  360. // 判断卡的连接方式
  361. async judgeConnectType(cardData) {
  362. try {
  363. // 设置上报参数
  364. this.logReportObj.setParams({videoType: cardData.videoCode.toLowerCase(), resourceId: cardData.resourceId});
  365. // 不支持webRTC跳转到指定的页面进行拉流
  366. if (!cardData.isWebrtc) {
  367. // 关闭日志上报
  368. this.logReportObj.destroy();
  369. // 跳转指定页面
  370. location.replace(`${location.origin}/h5/webRtcYJ/WXtrialInterface.html${location.search}`)
  371. return Promise.reject();
  372. }
  373. // 是否支持webRTC
  374. if (!this.isSupportRtc) {
  375. // 设置日志 推流状态为失败
  376. this.logReportObj.setParams({plugFowStatus: 2, linkEndTime: this.logReportObj.formatDate(new Date())});
  377. // 日志上报
  378. this.logReportObj.collectLog(`${+this.parametersData.isWeixin ? '微信小程序' : ''}当前版本暂不支持使用`);
  379. this.$dialog.alert({
  380. title: '提示',
  381. message: '当前环境不支持使用,可下载谷歌浏览器或客户端进行使用',
  382. confirmButtonText: '确定',
  383. confirmButtonColor: '#3cc51f',
  384. callback: (_, done) => {
  385. done();
  386. this.exit();
  387. }
  388. })
  389. return Promise.reject(new Error('当前浏览器不支持webRTC'));
  390. }
  391. // webRtc连接,需获取连接中转地址
  392. if (cardData.webrtcNetworkAnalysis) {
  393. // 如果有网络分析的请求地址, 则请求,否则失败
  394. const { data: webrtcNetworkAnalysisReq }= await request.get(cardData.webrtcNetworkAnalysis); // 这个接口单独使用axios请求, 因为返回的数据跟封装的数据结构不一统一,是其他平台的接口,所以单独请求
  395. if (webrtcNetworkAnalysisReq !== null && webrtcNetworkAnalysisReq.success && webrtcNetworkAnalysisReq.data) {
  396. // 保存获取的连接地址到connect的请求的响应中, 方便后面使用
  397. cardData.webrtcNetwork = webrtcNetworkAnalysisReq.data;
  398. // 设置上报参数
  399. this.logReportObj.setParams({transferServerIp: webrtcNetworkAnalysisReq.data});
  400. return cardData;
  401. }else{
  402. // 设置上报参数
  403. this.logReportObj.setParams({linkWay: 4, plugFowStatus: 2, linkEndTime: this.logReportObj.formatDate(new Date())});
  404. // 日志上报
  405. this.logReportObj.collectLog(
  406. `webRtc连接,获取中转地址失败:
  407. url: ${cardData.webrtcNetworkAnalysis}
  408. method: get
  409. 参数: 无
  410. 响应: ${JSON.stringify(webrtcNetworkAnalysisReq)}`
  411. );
  412. // 弹窗并退出
  413. this.$dialog.alert({
  414. title: '提示',
  415. message: '访问失败,请稍后重试',
  416. confirmButtonText: '确定',
  417. confirmButtonColor: '#3cc51f',
  418. beforeClose: (action, done) => {
  419. done()
  420. this.exit();
  421. }
  422. })
  423. return Promise.reject(new Error('网络分析请求失败'));
  424. }
  425. }else{
  426. // 设置上报参数
  427. this.logReportObj.setParams({linkWay: 4, plugFowStatus: 2});
  428. // 日志上报
  429. this.logReportObj.collectLog(
  430. `webRtc连接,获取请求中转地址为空:
  431. url: /api/resources/user/cloud/connect
  432. method: post
  433. 参数: ${JSON.stringify({ userCardId: this.parametersData.userCardId })}
  434. 响应: ${JSON.stringify(res)}`
  435. );
  436. return Promise.reject(new Error('网络分析请求地址不存在'));
  437. }
  438. } catch (error) {
  439. console.log('判断卡的连接方式', error);
  440. return Promise.reject(error);
  441. }
  442. },
  443. // 初始化webRTC及相关配置
  444. initWebRtc() {
  445. try {
  446. // 获取挂载的容器元素
  447. const videoRef = document.getElementById("videoRef");
  448. // 解构connectData中的数据
  449. const { sn: topic, cardToken: authToken, localIp, internetHttps, internetHttp, webrtcNetwork, webrtcTransferCmnet, webrtcTransferTelecom, webrtcTransferUnicom, videoCode } = this.connectData;
  450. // 判断长连接的协议方式
  451. const isWss = location.protocol === 'https:';
  452. // 生成连接地址
  453. const url = `${isWss ? 'wss://' : 'ws://'}${isWss ? internetHttps : internetHttp}/nats`;
  454. let quality = localStorage.getItem('definitionValue') ?? '自动';
  455. // 统一使用三网解析地址
  456. const ICEServerUrl = [
  457. { "CMNET": webrtcNetwork }, // 移动
  458. { 'CHINANET-GD': webrtcNetwork }, // 电信
  459. { 'UNICOM-GD': webrtcNetwork }, // 联通
  460. ];
  461. // 配置连接参数
  462. const connection = {
  463. mount: videoRef,
  464. displaySize: { // 视频在页面显示的尺寸 必填
  465. width: this.pageData.videoWidth,
  466. height: this.pageData.videoHeight,
  467. },
  468. topic, // SN号 必填
  469. url, //信令服务地址 必填
  470. ICEServerUrl,
  471. forwardServerAddress: '', // 转发服务器地址
  472. ip: localIp, // 实例ip
  473. controlToken: '', // 控制token
  474. width: 720, // 推流视频宽度 必填
  475. height: 1080, // 推流视频高度 必填
  476. cardWidth: 0, // 云机系统分辨率 宽 必填
  477. cardHeight: 0, // 云机系统分辨率 高 必填
  478. cardDensity: 0, // 云机系统显示 密度 必填
  479. authToken, //拉流鉴权 token 必填
  480. quality, // 画质(码率) 超清 | 高清 | 标清 | 流畅
  481. fps: 30, //必填
  482. videoCodec: videoCode, // 视频编码格式 必填
  483. videoCodecMethod: true, // 硬编true | 软编false
  484. isMuted: false, // 是否静音
  485. isAllowedOpenCamera: true, // 是否允许打开摄像头
  486. sendFollow: true, // 是否允许主控转发文本到实例
  487. callback: (event)=> {}
  488. };
  489. // 设置日志参数 推流质量
  490. this.logReportObj.setParams({imageQuality: quality});
  491. // 获取SDK类
  492. const MediaSdk = window.rtc_sdk.MediaSdk;
  493. // 初始化 SDK
  494. this.engine = new MediaSdk();
  495. console.log('RtcEngineConfig==', connection)
  496. // 初始化 SDK 并传入连接参数
  497. this.engine.RtcEngine(connection);
  498. // 监听回调方法
  499. this.eventCallbackFunction();
  500. } catch (error) {
  501. console.log('webRTC初始化失败', error);
  502. }
  503. },
  504. // webRTC状态回调监听回调方法
  505. eventCallbackFunction() {
  506. const engine = this.engine;
  507. // 连接成功
  508. engine.on('CONNECT_SUCCESS', (r) => {
  509. console.log("webrtc连接成功====★★★★★", r);
  510. if (r.code === 1005) { // 1005: 拉流鉴权成功
  511. // 设置日志 推流状态为成功
  512. let now = new Date();
  513. this.logReportObj.setParams({plugFowStatus: 1, linkWay: 1, timeConsuming: now.getTime() - this.logReportObj.timeStartTime, linkEndTime: this.logReportObj.formatDate(now)});
  514. // 日志上报
  515. this.logReportObj.collectLog(`拉流成功`);
  516. // 初始化业务指令通道
  517. this.initControlChannel();
  518. // 查询超过指定触碰时间是否提示关闭弹窗
  519. this.$refs.timeoutNoOpsRef.pushflowPopup();
  520. // 获取云机剩余时长
  521. this.$refs.timeBalanceRef.getResidueTime();
  522. }
  523. });
  524. // 连接关闭
  525. engine.on('CONNECT_CLOSE', (r) => {
  526. console.log("webrtc关闭====★★★★★", r);
  527. });
  528. // 网络连接统计信息监听
  529. engine.on('NETWORK_STATS', (r) => {
  530. this.rtcNetwork = r;
  531. });
  532. // 连接异常
  533. engine.on('CONNECT_ERROR', (r) => {
  534. console.log("webrtc异常状态====★★★★★", r);
  535. // 异常状态枚举值
  536. const statusEnum = {
  537. 10011: '连接超时',
  538. 10012: '连接失败',
  539. 1002: 'rtc连接失败',
  540. 1003: 'rtc异常断开',
  541. 1004: 'rtc连接失败',
  542. 1006: '鉴权失败',
  543. };
  544. // 设置日志 推流状态为失败
  545. this.logReportObj.setParams({plugFowStatus: 2, linkWay: 0, linkEndTime: this.logReportObj.formatDate(new Date())});
  546. // 日志上报
  547. this.logReportObj.collectLog( `${statusEnum[r.code] || '连接异常'}: 消息: ${JSON.stringify(r)}` );
  548. this.$dialog.alert({
  549. title: '提示',
  550. message: '链接超时',
  551. confirmButtonText: '确定',
  552. confirmButtonColor: '#3cc51f',
  553. beforeClose: (action, done) => {
  554. done()
  555. this.exit()
  556. }
  557. })
  558. });
  559. // 显示区域大小发生改变 响应
  560. engine.on('RECEIVE_RESOLUTION', (r) => {
  561. // 分辨率大小发生改变,响应
  562. console.log("分辨率大小发生改变,响应 => RECEIVE_RESOLUTION", r);
  563. });
  564. },
  565. // 业务指令通道初始化
  566. initControlChannel() {
  567. try {
  568. // 初始化业务指令通道
  569. let { internetHttps, internetHttp, localIp, cardToken } = this.connectData;
  570. const isWss = location.protocol === 'https:';
  571. let cUrl = `${isWss ? 'wss' : 'ws'}://${isWss ? internetHttps : internetHttp}/businessChannel?cardIp=${localIp}&token=${cardToken}&type=directives`;
  572. this.doConnectDirectivesWs = new WebSocket(cUrl);
  573. this.doConnectDirectivesWs.binaryType = 'arraybuffer';
  574. // 清除定时器
  575. if (this.doConnectDirectivesIntervalerPing) {
  576. clearInterval(this.doConnectDirectivesIntervalerPing);
  577. }
  578. // 链接成功
  579. this.doConnectDirectivesWs.onopen = (e) => {
  580. // 日志上报
  581. this.logReportObj.collectLog( `消息: 业务通道连接成功` );
  582. // 清除loading
  583. this.$toast.clear();
  584. // 重置重连次数
  585. this.doConnectDirectivesRequestNum = 1;
  586. // 设置定时器 每3秒发送一次心跳
  587. this.doConnectDirectivesIntervalerPing = setInterval(() => {
  588. if (this.doConnectDirectivesWs.readyState === 1) {
  589. this.sendWsCommand({ type: 'ping' });
  590. } else {
  591. clearInterval(this.doConnectDirectivesIntervalerPing);
  592. }
  593. }, 3000);
  594. // 输入法: 本地输入法 1 云机输入法 2
  595. this.sendWsCommand({ type: 'InputMethod', data: { type: 2 } });
  596. }
  597. // 接受到的消息
  598. this.doConnectDirectivesWs.onmessage = res => {
  599. const result = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
  600. switch (result.type) {
  601. case 'reProduceText':
  602. this.$native.clipboard.writeText(result.data.text);
  603. break
  604. case 'downAdnInstallRep':
  605. this.$toast(result.data.msg)
  606. break
  607. // 接受到这个消息就自动退出云机
  608. case 'exitPhone':
  609. this.exit();
  610. break
  611. }
  612. }
  613. // 链接报错的回调
  614. this.doConnectDirectivesWs.onerror = res => {
  615. // 设置日志
  616. this.logReportObj.setParams({plugFowStatus: 1, linkWay: 0, linkEndTime: this.logReportObj.formatDate(new Date())});
  617. // 日志上报
  618. this.logReportObj.collectLog(
  619. `业务指令通道报错:
  620. url: ${res.target.url}
  621. type: ${res.type}
  622. 消息: ${JSON.stringify(res)}`
  623. );
  624. if (this.doConnectDirectivesRequestNum > this.doConnectDirectivesRequestNumMax) {
  625. return this.exit();
  626. }else{
  627. // 重连次数加1
  628. ++this.doConnectDirectivesRequestNum;
  629. // 重连
  630. this.initControlChannel();
  631. }
  632. }
  633. } catch (error) {
  634. console.log('initControlChannel error', error);
  635. }
  636. },
  637. /**
  638. * popup 功能按钮点击事件
  639. * @param {Object} val 传递的参数
  640. * @description 传递的参数格式: { code: 'hideKey', label: '隐藏虚拟键', icon: 'icon-hide' }
  641. */
  642. funcHandle({code}) {
  643. console.log('funcHandle code:', code);
  644. switch (code) {
  645. case 'hideKey':
  646. // 隐藏虚拟按键
  647. document.getElementById('foot-menu-wrap').style.display = 'none';
  648. this.pageData.footMenuHeight = 0; // 设置底部菜单高度为0
  649. this.getInitSize(); // 重新计算视频尺寸
  650. this.changeVideoStyle(); // 重新设置视频尺寸
  651. break;
  652. case 'showKey':
  653. // 显示虚拟按键
  654. document.getElementById('foot-menu-wrap').style.display = 'flex';
  655. this.pageData.footMenuHeight = 40; // 设置底部菜单高度为40px
  656. this.getInitSize(); // 重新计算视频尺寸
  657. this.changeVideoStyle(); // 重新设置视频尺寸
  658. break
  659. case 'shearplate':
  660. // 粘贴板按钮
  661. this.shearplate();
  662. break;
  663. case 'shake':
  664. // 摇一摇
  665. if(this.sendWsCommand({ type: 'shakeit' })) {
  666. this.$toast('操作成功,如有异常请联系客服');
  667. } else {
  668. this.$toast('操作失败,请联系客服');
  669. }
  670. break;
  671. case 'blow':
  672. // 吹一吹
  673. if(this.sendWsCommand({ type: 'blow' })) {
  674. this.$toast('操作成功,如有异常请联系客服');
  675. } else {
  676. this.$toast('操作失败,请联系客服');
  677. }
  678. break;
  679. }
  680. },
  681. // 重新设置视频尺寸
  682. changeVideoStyle() {
  683. this.$nextTick(() => {
  684. // 获取video元素
  685. const video = document.getElementById("videoRef").getElementsByTagName('video')[0];
  686. video.style.width = this.pageData.videoWidth + 'px';
  687. video.style.height = this.pageData.videoHeight + 'px';
  688. });
  689. },
  690. // 三键
  691. sendKey (keyCode) {
  692. try {
  693. this.engine?.sendKey(keyCode);
  694. // 重置超时无操作定时器
  695. this.$refs.timeoutNoOpsRef?.noOperationSetTimeout();
  696. } catch (error) {
  697. console.log('sendKey error', error);
  698. }
  699. },
  700. // popup粘贴板按钮点击事件
  701. shearplate(){
  702. // 调用InputCopy组件的pasteText方法读取粘贴板
  703. this.$refs.inputCopyRef.pasteText();
  704. },
  705. // 打开去取云机内的粘贴板内容
  706. openPasteboard(text){
  707. this.$refs.cloudPhoneClipboardRef.init(text);
  708. },
  709. // 退出功能
  710. exit() {
  711. // 关闭日志上报
  712. this.logReportObj.destroy();
  713. // 关闭webRTC
  714. this.engine.disconnect && this.engine.disconnect();
  715. // 关闭业务指令通道
  716. this.doConnectDirectivesWs && this.doConnectDirectivesWs.close();
  717. // 获取当前环境
  718. const env = isBrowserEnvironment();
  719. // ios环境下 直接使用浏览器api返回上一页
  720. if(env.isBrowser && !env.isMiniProgramWebview && env.isIPhone && env.isTopWindow) {
  721. // 上面的方法执行未生效就走这里
  722. if (window.history.length > 1) {
  723. return window.history.back();
  724. }
  725. }
  726. uni && uni.reLaunch({ url: '/pages/index/index' });
  727. },
  728. // 发送业务指令通道消息
  729. sendWsCommand(data) {
  730. if(this.doConnectDirectivesWs.readyState === 1) {
  731. this.doConnectDirectivesWs.send(JSON.stringify(data));
  732. return true;
  733. }
  734. return false;
  735. },
  736. // 初始化日志上报实例
  737. initLogReport() {
  738. // 初始化日志上报实例
  739. this.logReportObj = new logReport({ request: this.$axios });
  740. uni.getEnv((res) => {
  741. // 设置上报参数
  742. this.logReportObj.setParams({ clientType: Object.keys(res)[0] });
  743. })
  744. },
  745. }
  746. }
  747. </script>
  748. <style lang="scss" scoped>
  749. html{
  750. background-color: #000;
  751. }
  752. // 动态生成 从 0 到 100px 的样式
  753. @for $i from 0 through 100 {
  754. .mb-#{$i} {
  755. margin-bottom: #{$i}px;
  756. }
  757. .mt-#{$i} {
  758. margin-top: #{$i}px;
  759. }
  760. .ml-#{$i} {
  761. margin-left: #{$i}px;
  762. }
  763. .mr-#{$i} {
  764. margin-right: #{$i}px;
  765. }
  766. }
  767. $-radeus-12: 12px;
  768. $-bg-yellow: rgb(255, 253, 241);
  769. .rtc-page{
  770. position: relative;
  771. font-size: 14px;
  772. .video-wrapper{
  773. position: relative;
  774. }
  775. }
  776. .cover-bg{
  777. background-color: #000;
  778. }
  779. #foot-menu-wrap{
  780. border-width: 0px;
  781. position: absolute;
  782. left: 0px;
  783. bottom: 0px;
  784. width: 100%;
  785. // height: 40px; // 三大功能键高度,通过vue动态添加
  786. background: inherit;
  787. background-color: rgba(0, 12, 23, 1);
  788. border: none;
  789. border-radius: 0px;
  790. -moz-box-shadow: none;
  791. -webkit-box-shadow: none;
  792. box-shadow: none;
  793. z-index: 1;
  794. display: flex;
  795. justify-content: space-evenly;
  796. align-items: center;
  797. color: #fff;
  798. }
  799. </style>