rtc.vue 28 KB

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