WXtrialInterface.js 47 KB

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