WXtrialInterfaceCopy.js 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222
  1. import request from './request.js'
  2. import { clickCopyText, pasteText } from './common.js'
  3. import logReport from './logReport.js'
  4. // 禁止双击缩放
  5. document.addEventListener('dblclick', function (e) {
  6. e.preventDefault();
  7. });
  8. // 添加监听 页面显示或隐藏 事件
  9. document.addEventListener('visibilitychange', visibilitychanged);
  10. // 监听 页面显示或隐藏 执行的函数
  11. function visibilitychanged() {
  12. // 获取当前环境
  13. const env = isBrowserEnvironment();
  14. // 获取当前页面的可见性状态
  15. const visibilityState = document.visibilityState;
  16. if (visibilityState === 'visible') {
  17. // 页面显示时的逻辑
  18. // 网页重载
  19. if (env.isBrowser && env.isTopWindow && env.isIPhone) {
  20. location.reload();
  21. }
  22. } else if (visibilityState === 'hidden') {
  23. // 页面隐藏时的逻辑
  24. // video.pause();
  25. } else if (visibilityState === 'prerender') {
  26. // 页面预渲染时的逻辑
  27. console.log('页面处于预渲染状态');
  28. } else if (visibilityState === 'unloaded') {
  29. // 页面即将卸载时的逻辑 移除监听
  30. document.removeEventListener('visibilitychange', visibilitychanged);
  31. }
  32. }
  33. /**
  34. * @description: 判断当前页面运行环境
  35. * @return {Object} 返回当前页面运行环境
  36. */
  37. function isBrowserEnvironment() {
  38. // 判断是否在浏览器环境中
  39. const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined';
  40. // 判断是否在微信环境中
  41. const isWechat = /MicroMessenger/i.test(navigator.userAgent);
  42. // 判断是否在微信小程序的 web-view 中
  43. const isMiniProgramWebview = isWechat && /miniProgram/i.test(navigator.userAgent);
  44. // 判断是否是 iPhone 设备
  45. const isIPhone = /iPhone/i.test(navigator.userAgent);
  46. // 判断是否是顶级窗口(不是嵌套在 iframe 中)目前只有ios的浏览器中才会是顶级窗口
  47. const isTopWindow = window.parent === window.self;
  48. return {
  49. isBrowser, // 是否在浏览器环境中
  50. isWechat, // 是否在微信环境中
  51. isMiniProgramWebview, // 是否在微信小程序的 web-view 中
  52. isIPhone, // 是否是 iPhone 设备
  53. isTopWindow, // 是否是顶级窗口
  54. };
  55. }
  56. /**
  57. * @description: 检查视频是否黑屏
  58. * @param {Number} threshold 阈值,根据需要调整 默认20
  59. * @return {Boolean} 返回是否为黑屏
  60. */
  61. function checkVideoBlackScreen(threshold = 20) { // 阈值,根据需要调整
  62. // 获取视频元素
  63. const video = document.getElementById('playerVideo');
  64. const canvas = document.createElement('canvas');
  65. const context = canvas.getContext('2d');
  66. canvas.width = video.videoWidth;
  67. canvas.height = video.videoHeight;
  68. // 绘制视频帧到画布上
  69. context.drawImage(video, 0, 0, canvas.width, canvas.height);
  70. // 获取图像数据
  71. const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
  72. const data = imageData.data;
  73. let totalBrightness = 0;
  74. // 遍历图像数据,计算亮度
  75. for (let i = 0; i < data.length; i += 4) {
  76. // 计算每个像素RGB(R:data[i], G:data[i + 1], B:data[i + 2])的亮度,这里使用简单的平均值
  77. const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3;
  78. // 累加亮度值
  79. totalBrightness += brightness;
  80. }
  81. // 判断亮度是否低于阈值
  82. const averageBrightness = totalBrightness / (data.length / 4); // 计算平均亮度
  83. let isBlackScreen = averageBrightness < threshold;
  84. console.log('平均亮度', averageBrightness);
  85. // 释放资源
  86. canvas.remove();
  87. // 返回是否为黑屏
  88. return isBlackScreen;
  89. }
  90. const { Dialog, Toast } = vant
  91. Toast.setDefaultOptions({ duration: 2000 });
  92. // 从 CLOUD_GAME_SDK 结构中解构必要的函数和常量
  93. const RtcEngineSDK = window.rtc_sdk.CloudGameSdk
  94. // 业务通道定时标识
  95. let doConnectDirectivesIntervalerPing = null
  96. // 获取云机数据定时标识
  97. let getUserCardInfoTimerInterval = null
  98. let getUserCardInfoRequestNum = 1
  99. let doConnectDirectivesRequestNum = 1
  100. let doConnectDirectivesTimerInterval = null
  101. // 触碰间隔定时标识
  102. let noOperationSetTimeoutTimeInterval = null
  103. let noOperationSetIntervalTimeInterval = null
  104. // 倒计时定时标识
  105. let countdownTimeInterval = null
  106. // 日志上报实例
  107. let logReportObj = null;
  108. const app = new Vue({
  109. el: '#app',
  110. data: {
  111. // 底部按钮
  112. footerBtn: [{
  113. key: 'task',
  114. img: '../static/img/wx/gengduo_icon.png'
  115. }, {
  116. key: 'home',
  117. img: '../static/img/wx/home_icon.png'
  118. }, {
  119. key: 'onBack',
  120. img: '../static/img/wx/fanhui_icon.png'
  121. }],
  122. // 宽高
  123. width: 0,
  124. height: 0,
  125. // webRtc实例
  126. engine: {},
  127. // 横竖屏幕 false是竖
  128. isLandscape: false,
  129. // 悬浮球位置
  130. levitatedSpherePositionData: {},
  131. // 右侧弹窗
  132. levitatedSphereVisible: false,
  133. // 清晰度数据
  134. definitionList: [{
  135. name: '高清',
  136. value: 2800
  137. }, {
  138. name: '标清',
  139. value: 2200,
  140. }, {
  141. name: '极速',
  142. value: 1800,
  143. }],
  144. // 选中的清晰度
  145. definitionValue: '',
  146. // 分辨率
  147. resolutionRatioVisible: false,
  148. resolutionRatioList: [],
  149. // 需要用到的参数
  150. parametersData: {},
  151. // 屏幕分辨率
  152. phoneSize: {},
  153. // 业务指令通道实例
  154. doConnectDirectivesWs: null,
  155. // 粘贴版
  156. pasteVersionVisible: false,
  157. pasteVersionList: [],
  158. // 复制内容
  159. copyTextValue: '',
  160. copyTextVisible: false,
  161. // 卡数据
  162. userCardInfoData: {},
  163. // 是否显示计时
  164. timingVisible: false,
  165. // 计费规则
  166. billingRulesVisible: false,
  167. // 应用推荐
  168. applyRecommendVisible: false,
  169. // 是否是启动不操作自动退出云机功能
  170. isFiringNoOperationSetTimeout: false,
  171. // 超过指定触碰时间的弹窗
  172. noOperationSetTimeoutTimeVisible: false,
  173. // 超过指定触碰时间的弹窗文案
  174. confirmButtonText: '',
  175. // 云机剩余时长
  176. countdownTime: 0,
  177. // 是否支持webRTC
  178. isSupportRtc: !!(
  179. typeof RTCPeerConnection !== 'undefined' &&
  180. typeof RTCIceCandidate !== 'undefined' &&
  181. typeof RTCSessionDescription !== 'undefined'
  182. ),
  183. // 推荐列表
  184. recommendList: [],
  185. layoutViewWidth: null,
  186. layoutViewHeight: null,
  187. // 是否显示video
  188. isShowVideo: false,
  189. plugFlowStartTime: null,
  190. },
  191. created() {
  192. this.initConfig()
  193. },
  194. mounted() {
  195. // 初始化日志上报
  196. this.initLogReport();
  197. // 获取卡信息,并连接云机拉流
  198. this.getUserCardInfo();
  199. },
  200. // 销毁实例前
  201. beforeDestroy() {
  202. console.log('销毁实例前');
  203. },
  204. computed: {
  205. // 右侧弹框退出相关按钮
  206. exitList() {
  207. let arr = [{
  208. name: '剪贴版',
  209. key: "shearplate",
  210. img: '../static/img/wx/jianqieban_icon.png'
  211. }, {
  212. name: '退出',
  213. key: 'signout',
  214. img: '../static/img/wx/tuichu_icon.png'
  215. }]
  216. if ([1, 2, 3].includes(+this.parametersData.userCardType)) {
  217. arr.push({
  218. name: '退出并下机',
  219. key: 'dormant',
  220. img: '../static/img/wx/tuichu_icon.png'
  221. })
  222. }
  223. return arr
  224. },
  225. rtcMediaPlayerStyle() {
  226. let obj = {
  227. objectFit: "fill",
  228. width: `${this.layoutViewWidth}px`,
  229. height: `${this.layoutViewHeight}px`,
  230. }
  231. if (this.isLandscape) {
  232. obj = {
  233. width: `${this.layoutViewHeight}px`,
  234. height: `${this.layoutViewWidth}px`,
  235. transform: 'rotate(90deg)'
  236. }
  237. }
  238. return obj
  239. }
  240. },
  241. methods: {
  242. // 初始化
  243. initConfig() {
  244. // 获取窗口尺寸
  245. this.getInitSize();
  246. // 获取缓存悬浮球位置
  247. let levitatedSpherePositionData = localStorage.getItem('levitatedSpherePositionData');
  248. // 获取缓存清晰度
  249. let definitionValue = localStorage.getItem('definitionValue')
  250. // 悬浮球位置
  251. this.levitatedSpherePositionData = levitatedSpherePositionData ? JSON.parse(levitatedSpherePositionData) : { right: '15px', top: '15px' }
  252. // 清晰度
  253. this.definitionValue = definitionValue ? +definitionValue : 2200
  254. // 获取参数
  255. this.parametersData = getParameters()
  256. let { token, merchantSign } = this.parametersData
  257. // 给api增加需要的参数
  258. request.defaults.headers.Authorization = token;
  259. request.defaults.headers.versionname = '5.9.1';
  260. // 获取商户标识, 打开云机时必传, 用于日志上报
  261. request.defaults.headers.merchantSign = merchantSign;
  262. window.onresize = () => {
  263. console.log('窗口尺寸变化');
  264. this.getInitSize()
  265. }
  266. },
  267. // 连接webRTC
  268. connectWebRtc() {
  269. const MediaSdk = window.rtc_sdk.MediaSdk;
  270. const videoRef = document.getElementById("videoRef");
  271. this.plugFlowStartTime = +new Date()
  272. const { sn: topic, cardToken: authToken, internetIp, internetHttps, internetHttp, webrtcNetwork, webrtcTransferCmnet, webrtcTransferTelecom, webrtcTransferUnicom, videoCode } = this.userCardInfoData;
  273. const isWss = location.protocol === 'https:'
  274. const url = `${isWss ? 'wss://' : 'ws://'}${isWss ? internetHttps : internetHttp}/nats`;
  275. const ICEServerUrl = [
  276. // 统一使用三网解析地址
  277. { "CMNET": webrtcNetwork }, // 移动
  278. { 'CHINANET-GD': webrtcNetwork }, // 电信
  279. { 'UNICOM-GD': webrtcNetwork }, // 联通
  280. ];
  281. const connection = {
  282. mount: videoRef,
  283. displaySize: { // 整个页面的大小
  284. width: this.layoutViewWidth,
  285. height: this.layoutViewHeight,
  286. },
  287. topic, // SN号 必填
  288. url, //信令服务地址 必填
  289. ICEServerUrl,
  290. forwardServerAddress: '', // 转发服务器地址
  291. ip: internetIp, // 实例ip
  292. controlToken: authToken, // 控制token
  293. width: 720, // 推流视频宽度 必填
  294. height: 1080, // 推流视频高度 必填
  295. cardWidth: 0, // 云机系统分辨率 宽 必填
  296. cardHeight: 0, // 云机系统分辨率 高 必填
  297. cardDensity: 0, // 云机系统显示 密度 必填
  298. authToken, //拉流鉴权 token 必填
  299. quality: '高清',// 画质(码率) 超清 | 高清 | 标清 | 流畅
  300. fps: 30, //必填
  301. videoCodec: videoCode, // 视频编码格式 必填
  302. videoCodecMethod: true, // 硬编true | 软编false
  303. isMuted: false, // 是否静音
  304. isAllowedOpenCamera: true, // 是否允许打开摄像头
  305. sendFollow: true, // 是否允许主控转发文本到实例
  306. callback: (event)=> {}
  307. }
  308. // 设置日志参数 推流质量
  309. logReportObj.setParams({imageQuality: 6000}); // 6000在日志上报枚举中为高清
  310. // 初始化 SDK
  311. this.engine = new MediaSdk();
  312. console.log("初始化 SDK", this.engine);
  313. this.engine.RtcEngine(connection);
  314. // 监听回调方法
  315. this.eventCallbackFunction();
  316. },
  317. // webRTC状态回调监听回调方法
  318. eventCallbackFunction() {
  319. // TODO 查询屏幕方向未做
  320. const engine = this.engine;
  321. // 连接成功
  322. engine.on('CONNECT_SUCCESS', (r) => {
  323. console.log("webrtc连接成功====★★★★★", r);
  324. Toast.clear();
  325. // 设置日志 推流状态为成功
  326. let now = new Date();
  327. logReportObj.setParams({plugFowStatus: 1, linkWay: 1, timeConsuming: now.getTime() - logReportObj.timeStartTime, linkEndTime: logReportObj.formatDate(now)});
  328. // 日志上报
  329. logReportObj.collectLog(`推流成功`);
  330. if (r.code === 1005) {
  331. // 重连
  332. if (this.getUserCardInfoRequestNum > 1) {
  333. engine.setControlAudio(true); // 设置音频
  334. engine.changeTouchMode(true);
  335. // 重连成功后重置请求次数
  336. this.getUserCardInfoRequestNum = 1;
  337. } else {
  338. engine.changeTouchMode(true);
  339. }
  340. }
  341. });
  342. engine.on('CONNECT_CLOSE', (r) => {
  343. console.log("webrtc关闭====★★★★★", r);
  344. });
  345. engine.on('CONNECT_ERROR', (r) => {
  346. console.log("webrtc异常状态====★★★★★", r);
  347. // 设置日志 推流状态为失败
  348. logReportObj.setParams({plugFowStatus: 2, linkWay: 0, linkEndTime: logReportObj.formatDate(new Date())});
  349. // 日志上报
  350. logReportObj.collectLog(
  351. `webrtc异常状态
  352. 消息: ${JSON.stringify(r)}`
  353. );
  354. Dialog.alert({
  355. title: '提示',
  356. message: '链接超时',
  357. confirmButtonText: '确定',
  358. confirmButtonColor: '#3cc51f',
  359. beforeClose: (action, done) => {
  360. this.exit()
  361. done()
  362. }
  363. })
  364. });
  365. engine.on('keyboardFeedbackBean', (r) => {
  366. console.log("打开键盘事件====★★★★★", r);
  367. });
  368. engine.on('RECEIVE_CHANNEL', (r) => {
  369. console.log("通道信息====★★★★★", r);
  370. });
  371. engine.on('RECEIVE_ICE', (r) => {
  372. console.log("三网地址====★★★★★", r);
  373. });
  374. engine.on('NETWORK_STATS', (r) => {
  375. // console.log("网络信息统计", r);
  376. });
  377. // 解码状态
  378. engine.on("DECODING_STATUS", (r) => {
  379. const statusMessages = {
  380. 10071: "rtc连接成功时间",
  381. 10072: "rtc连接失败时间",
  382. 10073: "鉴权成功时间",
  383. 1007: "渲染数据时间",
  384. 10074: "nats连接成功时间",
  385. 10075: "rtc连接失败时间",
  386. 10076: "rtc连接超时时间",
  387. 10077: "获取三网配置成功时间",
  388. 10078: "没有配置三网时间",
  389. 10079: "获取三网配置失败时间",
  390. 10080: "获取三网配置超时时间"
  391. };
  392. const message = statusMessages[r.code] || `未知的状态码: ${r.code}`;
  393. console.log(`${message}:`, r);
  394. });
  395. },
  396. // 悬浮球click事件
  397. onSphere(e){
  398. this.levitatedSphereVisible = true
  399. e.preventDefault();
  400. },
  401. // 悬浮球按下事件
  402. onSphereDown(e){
  403. // 给元素设置鼠标按下状态
  404. e.target.isMousedown = true;
  405. e.preventDefault();
  406. },
  407. // 悬浮球抬起事件
  408. onSphereUp(e){
  409. // 给元素设置鼠标按下状态
  410. e.target.isMousedown = false;
  411. e.preventDefault();
  412. },
  413. // 悬浮球移动
  414. touchmoveLevitatedSphere(e) {
  415. // 过滤未按下时的移动事件
  416. if(e.type === 'mousemove' && !e.target.isMousedown) return
  417. let pageX,pageY;
  418. if(e.type === 'mousemove' && e.target.isMousedown){
  419. pageX = e.pageX;
  420. pageY = e.pageY;
  421. }else if(e.type === 'touchmove'){
  422. // let { pageX, pageY } = e.targetTouches[0]
  423. pageX = e.targetTouches[0].pageX;
  424. pageY = e.targetTouches[0].pageY;
  425. }
  426. let min = 20
  427. let MaxPageX = this.width - 20
  428. let MaxPageY = this.height - 20
  429. pageX = pageX <= min ? min : (pageX >= MaxPageX ? MaxPageX : pageX)
  430. pageY = pageY <= min ? min : (pageY >= MaxPageY ? MaxPageY : pageY)
  431. this.levitatedSpherePositionData = {
  432. left: `${pageX}px`,
  433. top: `${pageY}px`,
  434. transform: 'translate(-50%, -50%)'
  435. }
  436. e.preventDefault();
  437. },
  438. touchendLevitatedSphere(e) {
  439. localStorage.setItem('levitatedSpherePositionData', JSON.stringify(this.levitatedSpherePositionData))
  440. },
  441. // 清晰度
  442. definitionFun(value) {
  443. this.definitionValue = value
  444. this.engine.makeBitrate && this.engine.makeBitrate(value)
  445. // 设置日志参数 推流质量
  446. logReportObj.setParams({imageQuality: value});
  447. localStorage.setItem('definitionValue', value)
  448. this.levitatedSphereVisible = false
  449. },
  450. // 修改分辨率
  451. resolutionRatio() {
  452. request.get('/api/resources/v5/machine/resolution/getResolvingPower', { params: { userCardId: this.parametersData.userCardId } }).then(res => {
  453. if (res.success) {
  454. this.resolutionRatioList = res.data.map(item => {
  455. item.height = item.high
  456. return item
  457. })
  458. this.levitatedSphereVisible = false
  459. this.resolutionRatioVisible = true
  460. }
  461. })
  462. },
  463. // 确定修改分辨率
  464. confirmResolution() {
  465. let { width, height, dpi: density } = this.phoneSize
  466. this.engine.makeResolution && this.engine.makeResolution({ width, height, density })
  467. this.resolutionRatioVisible = false
  468. },
  469. // 退出相关按钮操作
  470. exitFun(key) {
  471. console.log(key)
  472. switch (key) {
  473. case 'dormant':
  474. Dialog.alert({
  475. title: '提示',
  476. message: '确定退出云手机并下机',
  477. confirmButtonText: '确定',
  478. confirmButtonColor: '#3cc51f',
  479. showCancelButton: true,
  480. beforeClose: (action, done) => {
  481. if (action === 'cancel') done()
  482. if (action === 'confirm') {
  483. this.downline(done)
  484. }
  485. }
  486. })
  487. break;
  488. case 'shearplate':
  489. this.copyTextValue = ''
  490. pasteText().then(content => {
  491. typeof content !== 'boolean' ? this.openPasteboard(content) : this.copyTextVisible = true
  492. }, err => {
  493. this.copyTextVisible = true
  494. })
  495. break;
  496. case 'signout':
  497. this.exit()
  498. break;
  499. }
  500. },
  501. // 业务指令
  502. doConnectDirectives() {
  503. let { internetHttps, internetHttp, localIp, cardToken } = this.userCardInfoData
  504. const isWss = location.protocol === 'https:'
  505. let cUrl = `${isWss ? 'wss' : 'ws'}://${isWss ? internetHttps : internetHttp}/businessChannel?cardIp=${localIp}&token=${cardToken}&type=directives`
  506. this.doConnectDirectivesWs = new WebSocket(cUrl);
  507. this.doConnectDirectivesWs.binaryType = 'arraybuffer'
  508. clearInterval(doConnectDirectivesIntervalerPing)
  509. // 链接成功
  510. this.doConnectDirectivesWs.onopen = (e) => {
  511. doConnectDirectivesIntervalerPing = setInterval(() => {
  512. if (this.doConnectDirectivesWs.readyState === 1) {
  513. this.doConnectDirectivesWs.send(JSON.stringify({ type: 'ping' }));
  514. } else {
  515. clearInterval(doConnectDirectivesIntervalerPing);
  516. }
  517. }, 3000)
  518. this.doConnectDirectivesWs.send(JSON.stringify({ type: 'getVsStatus' }))
  519. this.doConnectDirectivesWs.send(JSON.stringify({ type: 'bitRate', data: { bitRate: 1243000 } }))
  520. // 设置日志参数 推流质量
  521. logReportObj.setParams({imageQuality: 1243000});
  522. this.doConnectDirectivesWs.send(JSON.stringify({ type: 'InputMethod', data: { type: 2 } }))
  523. this.doConnectDirectivesWs.send(JSON.stringify({ type: 'getPhoneSize' }))
  524. }
  525. // 接受到的消息
  526. this.doConnectDirectivesWs.onmessage = res => {
  527. const result = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
  528. switch (result.type) {
  529. // 分辨率
  530. case 'getPhoneSize':
  531. case 'setPhoneSize':
  532. let data = JSON.parse(JSON.stringify(result.data))
  533. let { width, height } = data
  534. if (width > height) {
  535. data.width = height
  536. data.height = width
  537. }
  538. this.phoneSize = data
  539. break
  540. // 云机复制过来的文本
  541. case 'reProduceText':
  542. if (navigator.clipboard) {
  543. navigator.clipboard.writeText(result.data.text);
  544. }
  545. break
  546. case 'downAdnInstallRep':
  547. Toast(result.data.msg)
  548. break
  549. // 接受到这个消息就自动退出云机
  550. case 'exitPhone':
  551. this.exit()
  552. break
  553. }
  554. }
  555. // 链接报错的回调
  556. this.doConnectDirectivesWs.onerror = res => {
  557. // 设置日志 推流状态为失败
  558. logReportObj.setParams({plugFowStatus: 1, linkWay: 0, linkEndTime: logReportObj.formatDate(new Date())});
  559. // 日志上报
  560. logReportObj.collectLog(
  561. `业务指令通道报错:
  562. url: ${res.target.url}
  563. type: ${res.type}
  564. 消息: ${JSON.stringify(res)}`
  565. );
  566. clearInterval(doConnectDirectivesTimerInterval)
  567. if (doConnectDirectivesRequestNum > 6) {
  568. this.exit()
  569. return
  570. }
  571. doConnectDirectivesRequestNum++
  572. this.doConnectDirectives()
  573. }
  574. },
  575. // 粘贴版相关接口
  576. shearContent({ type, params, queryStr }) {
  577. let url = '/api/public/v5/shear/content'
  578. if (queryStr) url += queryStr
  579. return request[type](url, params)
  580. },
  581. // 清空全部、清除某条
  582. deletePasteVersion(ids) {
  583. if (!ids) {
  584. Dialog.alert({
  585. title: '提示',
  586. message: '确定清空剪贴板?',
  587. confirmButtonText: '确定',
  588. confirmButtonColor: '#3cc51f',
  589. showCancelButton: true,
  590. beforeClose: (action, done) => {
  591. if (action === 'cancel') done()
  592. if (action === 'confirm') {
  593. fun.bind(this)(done)
  594. }
  595. }
  596. })
  597. return
  598. }
  599. fun.bind(this)()
  600. function fun(callBack = () => { }) {
  601. this.shearContent({
  602. type: 'delete', queryStr: Qs.stringify(
  603. {
  604. ids: ids ? [ids] : this.pasteVersionList.map((v) => v.id),
  605. },
  606. { arrayFormat: 'repeat', addQueryPrefix: true },
  607. )
  608. }).then(res => {
  609. if (res.status === 0) {
  610. this.getPasteVersion()
  611. callBack(true)
  612. } else {
  613. callBack(false)
  614. Toast(res.msg)
  615. }
  616. }).catch(() => {
  617. callBack(false)
  618. })
  619. }
  620. },
  621. // 获取粘贴版数据
  622. getPasteVersion(callBack = () => { }) {
  623. this.shearContent({ type: 'get' }).then(res => {
  624. this.pasteVersionList = res.data
  625. callBack(true)
  626. }).catch(() => {
  627. callBack(false)
  628. }).finally(() => { })
  629. },
  630. // 复制弹窗是否关闭
  631. beforeCloseCopy(action, done) {
  632. if (action !== 'confirm') {
  633. // 获取剪切板
  634. this.getPasteVersion(() => {
  635. this.pasteVersionVisible = true
  636. this.levitatedSphereVisible = false
  637. done()
  638. })
  639. return
  640. }
  641. if (!this.copyTextValue) {
  642. Toast('请输入复制到剪切板的内容')
  643. done(false)
  644. return
  645. }
  646. this.openPasteboard(this.copyTextValue, done)
  647. },
  648. // 打开粘贴板
  649. async openPasteboard(content, callBack = () => { }) {
  650. this.shearContent({ type: 'post', params: { content } }).then().finally(() => {
  651. callBack()
  652. // 获取剪切板
  653. this.getPasteVersion(() => {
  654. this.pasteVersionVisible = true
  655. this.levitatedSphereVisible = false
  656. })
  657. })
  658. },
  659. // 复制粘贴某条数据
  660. copyPasteVersiontext(e) {
  661. clickCopyText(e, (event) => {
  662. this.doConnectDirectivesWs.send(JSON.stringify({
  663. type: 'cutting',
  664. data: {
  665. str: event.text,
  666. },
  667. }))
  668. Toast('复制成功')
  669. }, () => {
  670. Toast('复制失败')
  671. })
  672. },
  673. // 获取卡信息
  674. getUserCardInfo() {
  675. Toast.loading({
  676. duration: 0, // 持续展示 toast
  677. message: '数据加载中...',
  678. });
  679. // 从url中获取userCardId
  680. let { userCardId } = this.parametersData
  681. userCardId = +userCardId
  682. const statusTips = {
  683. // 5200:RBD资源挂载中
  684. 5200: '网络异常,请稍后重试',
  685. // 入使用排队9.9,年卡
  686. 5220: '云手机正在一键修复中',
  687. 5203: '正在排队中,请稍等',
  688. // 9.9年卡连接异常,重新进入排队
  689. 5204: '云机异常,正在为你重新分配云机',
  690. 5228: '卡的网络状态为差',
  691. 5229: '接口返回链接信息缺失',
  692. }
  693. let { isWeixin } = this.parametersData;
  694. let clientType = +isWeixin ? 'wx' : undefined;
  695. // 设置上报参数
  696. logReportObj.setParams({userCardId});
  697. clientType && logReportObj.setParams({clientType});
  698. // 获取卡信息
  699. request.post('/api/resources/user/cloud/connect', { userCardId }).then(async res => {
  700. try {
  701. if (!res.success) {
  702. // 设置日志 推流状态为失败,和链接状态
  703. logReportObj.setParams({plugFowStatus: 2, linkWay: logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: logReportObj.formatDate(new Date())});
  704. // 日志上报
  705. logReportObj.collectLog(
  706. `接口获取数据失败:
  707. url: /api/resources/user/cloud/connect
  708. method: post
  709. 参数: ${JSON.stringify({ userCardId })}
  710. 响应: ${JSON.stringify(res)}`
  711. );
  712. return;
  713. }
  714. const data = res.data;
  715. // 设置上报参数
  716. logReportObj.setParams({videoType: data.videoCode.toLowerCase(), resourceId: data.resourceId});
  717. switch (res.status) {
  718. case 0:
  719. getUserCardInfoRequestNum = 1
  720. // 不支持webRTC跳转到指定的页面
  721. if (!res.data.isWebrtc) {
  722. // 关闭日志上报
  723. logReportObj.destroy();
  724. // 跳转指定页面
  725. location.replace(`${location.origin}/h5/webRtcYJ/WXtrialInterface.html${location.search}`)
  726. return
  727. }
  728. if (!this.isSupportRtc) {
  729. // 设置日志 推流状态为失败
  730. logReportObj.setParams({plugFowStatus: 2, linkEndTime: logReportObj.formatDate(new Date())});
  731. // 日志上报
  732. logReportObj.collectLog(`${+isWeixin ? '微信小程序' : ''}当前版本暂不支持使用`);
  733. Dialog.alert({
  734. title: '提示',
  735. message: `${+isWeixin ? '微信小程序' : ''}当前版本暂不支持使用,可下载谷歌浏览器或客户端进行使用`,
  736. confirmButtonText: '确定',
  737. confirmButtonColor: '#3cc51f',
  738. beforeClose: (action, done) => {
  739. this.exit()
  740. done()
  741. }
  742. })
  743. return
  744. }
  745. // webRtc连接,需获取连接中转地址
  746. if (res.data.webrtcNetworkAnalysis) {
  747. // 如果有网络分析的请求地址, 则请求,则否失败
  748. const webrtcNetworkAnalysisReq = await request.get(res.data.webrtcNetworkAnalysis);
  749. if (webrtcNetworkAnalysisReq !== null && webrtcNetworkAnalysisReq.success) {
  750. if (webrtcNetworkAnalysisReq.data) {
  751. // 保存获取的连接地址到上个请求的响应中, 方便后面使用
  752. res.data.webrtcNetwork = webrtcNetworkAnalysisReq.data;
  753. // 设置上报参数
  754. logReportObj.setParams({transferServerIp: webrtcNetworkAnalysisReq.data});
  755. }else{
  756. // 设置上报参数
  757. logReportObj.setParams({linkWay: 4, plugFowStatus: 2});
  758. // 日志上报
  759. logReportObj.collectLog(
  760. `webRtc连接,获取中转地址成功,但返回数据为空:
  761. url: ${res.data.webrtcNetworkAnalysis}
  762. method: get
  763. 参数: 无
  764. 响应: ${JSON.stringify(webrtcNetworkAnalysisReq)}`
  765. );
  766. }
  767. }else{
  768. // 设置上报参数
  769. logReportObj.setParams({linkWay: 4, plugFowStatus: 2, linkEndTime: logReportObj.formatDate(new Date())});
  770. // 日志上报
  771. logReportObj.collectLog(
  772. `webRtc连接,获取中转地址失败:
  773. url: ${res.data.webrtcNetworkAnalysis}
  774. method: get
  775. 参数: 无
  776. 响应: ${JSON.stringify(webrtcNetworkAnalysisReq)}`
  777. );
  778. }
  779. }else{
  780. // 设置上报参数
  781. logReportObj.setParams({linkWay: 4, plugFowStatus: 2});
  782. // 日志上报
  783. logReportObj.collectLog(
  784. `webRtc连接,获取请求中转地址为空:
  785. url: /api/resources/user/cloud/connect
  786. method: post
  787. 参数: ${JSON.stringify({ userCardId })}
  788. 响应: ${JSON.stringify(res)}`
  789. );
  790. }
  791. // 如果没有获取到连接地址, 则失败
  792. if(!res.data.webrtcNetwork) {
  793. Dialog.alert({
  794. title: '提示',
  795. message: '访问失败,请稍后重试',
  796. confirmButtonText: '确定',
  797. confirmButtonColor: '#3cc51f',
  798. beforeClose: (action, done) => {
  799. this.exit()
  800. done()
  801. }
  802. })
  803. return;
  804. }
  805. this.userCardInfoData = res.data
  806. // 进入连接云机准备阶段
  807. this.connectWebRtc();
  808. return
  809. case 5200:
  810. case 5220:
  811. case 5203:
  812. case 5204:
  813. Toast(statusTips[res.status]);
  814. // 设置上报参数
  815. logReportObj.setParams({linkWay: logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: logReportObj.formatDate(new Date())});
  816. // 日志上报
  817. logReportObj.collectLog(statusTips[res.status] ||
  818. `webRtc连接,获取请求中转地址返回异常:
  819. url: /api/resources/user/cloud/connect
  820. method: post
  821. 参数: ${JSON.stringify({ userCardId })}
  822. 响应: ${JSON.stringify(res)}`
  823. );
  824. break
  825. default:
  826. // 设置上报参数
  827. logReportObj.setParams({linkWay: logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: logReportObj.formatDate(new Date())});
  828. // 日志上报
  829. logReportObj.collectLog(statusTips[res.status] ||
  830. `webRtc连接,获取请求中转地址返回异常:
  831. url: /api/resources/user/cloud/connect
  832. method: post
  833. 参数: ${JSON.stringify({ userCardId })}
  834. 响应: ${JSON.stringify(res)}`
  835. );
  836. Toast('画面异常,请重新进入')
  837. break
  838. }
  839. setTimeout(() => {
  840. this.exit()
  841. }, 3000)
  842. } catch (error) {
  843. }
  844. }).catch((error) => {
  845. // 设置上报参数
  846. logReportObj.setParams({linkWay: 4, plugFowStatus: 2, linkEndTime: logReportObj.formatDate(new Date())});
  847. // 日志上报
  848. logReportObj.collectLog(
  849. `接口获取数据失败:
  850. url: /api/resources/user/cloud/connect
  851. method: post
  852. 参数: ${JSON.stringify({ userCardId })}
  853. 响应: ${JSON.stringify(error)}`
  854. );
  855. })
  856. // 重连
  857. function reconnect() {
  858. // 重连6次结束
  859. if (getUserCardInfoRequestNum > 6) {
  860. Toast('网络异常,请稍后重试')
  861. clearTimeout(getUserCardInfoTimerInterval)
  862. setTimeout(() => {
  863. this.exit()
  864. }, 3000)
  865. return
  866. }
  867. // 重连
  868. getUserCardInfoTimerInterval = setTimeout(() => {
  869. this.getUserCardInfo()
  870. getUserCardInfoRequestNum++
  871. }, 3000)
  872. }
  873. },
  874. // 超过指定触碰时间是否提示关闭链接
  875. pushflowPopup() {
  876. request.get('/api/public/v5/pushflow/popup').then(res => {
  877. if (res.success) {
  878. this.isFiringNoOperationSetTimeout = res.data
  879. this.noOperationSetTimeout()
  880. }
  881. })
  882. },
  883. // 退出功能
  884. exit() {
  885. // 关闭日志上报
  886. logReportObj.destroy();
  887. this.engine.disconnect && this.engine.disconnect();
  888. this.doConnectDirectivesWs && this.doConnectDirectivesWs.close()
  889. uni.reLaunch({
  890. url: '/pages/index/index'
  891. });
  892. // 上面的方法执行未生效就走这里
  893. if (window.history.length > 1) {
  894. window.history.back();
  895. }
  896. },
  897. // 不触碰屏幕显示退出链接弹窗
  898. noOperationSetTimeout(key) {
  899. if (!this.isFiringNoOperationSetTimeout) return
  900. clearTimeout(noOperationSetTimeoutTimeInterval)
  901. if (key === 'cancel') {
  902. clearInterval(noOperationSetIntervalTimeInterval)
  903. this.noOperationSetTimeoutTimeVisible = false
  904. this.noOperationSetTimeout()
  905. return
  906. }
  907. noOperationSetTimeoutTimeInterval = setTimeout(() => {
  908. let index = 9
  909. this.confirmButtonText = '退出(10秒)'
  910. this.noOperationSetTimeoutTimeVisible = true
  911. noOperationSetIntervalTimeInterval = setInterval(() => {
  912. this.confirmButtonText = `退出${index ? `(${index}秒)` : ''}`
  913. index--
  914. if (index < 0) {
  915. this.noOperationSetTimeout('cancel')
  916. this.exit()
  917. }
  918. }, 1000)
  919. }, 300000)
  920. },
  921. // 获取云机剩余时长
  922. async getResidueTime() {
  923. clearInterval(countdownTimeInterval)
  924. const { userCardType, isShowCountdown, isShowRule } = this.parametersData
  925. const { userCardId } = this.userCardInfoData
  926. if (![1, 2, 3].includes(+userCardType)) return
  927. const res = await request.get(`/api/resources/yearMember/getResidueTime?userCardId=${userCardId}`)
  928. let time = res.data;
  929. if (!res.status) {
  930. this.countdownTime = residueTimeStamp(time)
  931. await request.get(`/api/resources/yearMember/startTime?userCardId=${userCardId}`)
  932. if (+isShowCountdown) this.timingVisible = true
  933. if (+isShowRule) this.billingRulesVisible = true
  934. countdownTimeInterval = setInterval(() => {
  935. if (time <= 0) {
  936. clearInterval(countdownTimeInterval)
  937. this.downline()
  938. return
  939. }
  940. time--
  941. this.countdownTime = residueTimeStamp(time)
  942. }, 1000)
  943. }
  944. },
  945. // 关闭倒计时弹窗
  946. handlecountdownTimeClose() {
  947. const { userCardId } = this.userCardInfoData
  948. request.get(`/api/resources/yearMember/closeRemind?userCardId=${userCardId}`).then(res => {
  949. if (!res.status) {
  950. clearInterval(countdownTimeInterval)
  951. this.timingVisible = false
  952. return
  953. }
  954. Toast(res.msg);
  955. })
  956. },
  957. // 退出并下机
  958. downline(fun = () => { }) {
  959. const { userCardId } = this.userCardInfoData
  960. request.get(`/api/resources/yearMember/downline?userCardId=${userCardId}`).then(res => {
  961. if (!res.status) {
  962. fun(true)
  963. // 通信给h5项目告知是退出并下机
  964. parent.postMessage(
  965. {
  966. type: 'exit',
  967. },
  968. '*',
  969. );
  970. uni.postMessage({
  971. data: {
  972. type: 'exit'
  973. }
  974. });
  975. this.exit()
  976. return
  977. }
  978. fun(false)
  979. Toast(res.msg);
  980. })
  981. },
  982. // 获取推荐列表
  983. getRecommend() {
  984. const { userCardId } = this.userCardInfoData
  985. request.get(`/api/public/v1/market/get/recommend?userCardId=${userCardId}`).then(res => {
  986. if (!res.status) {
  987. this.billingRulesVisible = false
  988. this.recommendList = res.data
  989. this.recommendList.length && (this.applyRecommendVisible = true)
  990. }
  991. })
  992. },
  993. // 下载apk
  994. downAndInstallApk({ downloadUrl: apkUrl, id: taskUid }) {
  995. this.doConnectDirectivesWs.send(JSON.stringify({
  996. type: 'downAndInstallApk',
  997. data: {
  998. apkUrl,
  999. taskUid
  1000. },
  1001. }))
  1002. },
  1003. // 返回、主页、任务器
  1004. footerBtnFun(key) {
  1005. this.engine[key] && this.engine[key]()
  1006. this.noOperationSetTimeout()
  1007. },
  1008. // 获取初始化尺寸
  1009. getInitSize() {
  1010. // 高度、悬浮球相关配置
  1011. this.height = window.innerHeight;
  1012. this.width = window.innerWidth;
  1013. this.$nextTick(() => {
  1014. // 云机画面宽高
  1015. let layoutView = document.querySelector('.layout-view')
  1016. // 获取视口宽度,webRTC需要做成16:9的画面
  1017. let currentWidth = layoutView.clientWidth;
  1018. let currentHeight = layoutView.clientHeight;
  1019. // 计算当前视口的宽高比
  1020. const currentRatio = currentWidth / currentHeight;
  1021. // 9:16 的目标比例
  1022. const targetRatio = 9 / 16;
  1023. console.log(`当前视口的宽高比: ${currentRatio}`);
  1024. // 判断当前视口的宽高比与目标比例的关系
  1025. if (currentRatio > targetRatio) {
  1026. // 当前视口的宽高比大于目标比例,说明宽度“过宽”,需要以高度为基准
  1027. console.log("当前视口宽度过宽,应以高度为基准调整宽度");
  1028. this.layoutViewWidth = currentHeight * targetRatio;
  1029. this.layoutViewHeight = currentHeight;
  1030. console.log(`1目标: 宽${this.layoutViewWidth},高${this.layoutViewHeight}`);
  1031. } else {
  1032. // 当前视口的宽高比小于目标比例,说明高度“过高”,需要以宽度为基准
  1033. console.log("当前视口高度过高,应以宽度为基准调整高度");
  1034. this.layoutViewHeight = currentWidth / targetRatio;
  1035. this.layoutViewWidth = currentWidth;
  1036. console.log(`2目标: 宽${this.layoutViewWidth},高${this.layoutViewHeight}`);
  1037. }
  1038. // 悬浮球位置设置为默认位置
  1039. this.levitatedSpherePositionData = { right: '15px', top: '15px' }
  1040. })
  1041. },
  1042. // 音量
  1043. volumeControl(value) {
  1044. this.engine.ExexuteKeyBoard && this.engine.ExexuteKeyBoard(value)
  1045. this.$refs.rtcMediaPlayer && (this.$refs.rtcMediaPlayer.muted = false)
  1046. },
  1047. // 初始化日志上报实例
  1048. initLogReport() {
  1049. // 初始化日志上报实例
  1050. logReportObj = new logReport({ request });
  1051. uni.getEnv((res) => {
  1052. // 设置上报参数
  1053. logReportObj.setParams({clientType: Object.keys(res)[0]});
  1054. })
  1055. }
  1056. }
  1057. })
  1058. // 播放按钮点击事件
  1059. function playOnBtn() {
  1060. const { isTips } = this.parametersData;
  1061. Dialog.alert({
  1062. title: '提示',
  1063. message: `${+isTips ? '开始' : '继续'}使用云手机`,
  1064. confirmButtonText: '确定',
  1065. confirmButtonColor: '#3cc51f',
  1066. beforeClose: (action, done) => {
  1067. if (action === 'confirm') {
  1068. this.isShowVideo = true
  1069. Toast.clear();
  1070. this.$refs.rtcMediaPlayer.play()
  1071. setTimeout(() => {
  1072. this.doConnectDirectives()
  1073. this.pushflowPopup()
  1074. this.getResidueTime()
  1075. done()
  1076. })
  1077. }
  1078. }
  1079. });
  1080. }
  1081. // 获取URL参数
  1082. function getParameters() {
  1083. let arr = location.search.split('?')
  1084. let obj = {}
  1085. if (arr[1]) {
  1086. arr = arr[1].split('&')
  1087. arr.forEach(item => {
  1088. let [key, value = ''] = item.split('=')
  1089. obj[key] = value
  1090. })
  1091. }
  1092. return obj
  1093. }
  1094. // 倒计时处理的时间
  1095. function residueTimeStamp(value) {
  1096. let theTime = value;//秒
  1097. let middle = 0;//分
  1098. let hour = 0;//小时
  1099. if (theTime > 59) {
  1100. middle = parseInt(theTime / 60);
  1101. theTime = parseInt(theTime % 60);
  1102. }
  1103. if (middle > 59) {
  1104. hour = parseInt(middle / 60);
  1105. middle = parseInt(middle % 60);
  1106. }
  1107. theTime < 10 ? theTime = '0' + theTime : theTime = theTime
  1108. middle < 10 ? middle = '0' + middle : middle = middle
  1109. hour < 10 ? hour = '0' + hour : hour = hour
  1110. return hour + ':' + middle + ':' + theTime
  1111. }