import request from './request.js' import { clickCopyText, pasteText } from './common.js' import logReport from './logReport.js' // 禁止双击缩放 document.addEventListener('dblclick', function (e) { e.preventDefault(); }); // 添加监听 页面显示或隐藏 事件 document.addEventListener('visibilitychange', visibilitychanged); // 监听 页面显示或隐藏 执行的函数 function visibilitychanged() { // 获取当前环境 const env = isBrowserEnvironment(); // 获取当前页面的可见性状态 const visibilityState = document.visibilityState; if (visibilityState === 'visible') { // 页面显示时的逻辑 // 网页重载 if (env.isBrowser && env.isTopWindow && env.isIPhone) { location.reload(); } } else if (visibilityState === 'hidden') { // 页面隐藏时的逻辑 // video.pause(); } else if (visibilityState === 'prerender') { // 页面预渲染时的逻辑 console.log('页面处于预渲染状态'); } else if (visibilityState === 'unloaded') { // 页面即将卸载时的逻辑 移除监听 document.removeEventListener('visibilitychange', visibilitychanged); } } /** * @description: 判断当前页面运行环境 * @return {Object} 返回当前页面运行环境 */ function isBrowserEnvironment() { // 判断是否在浏览器环境中 const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof navigator !== 'undefined'; // 判断是否在微信环境中 const isWechat = /MicroMessenger/i.test(navigator.userAgent); // 判断是否在微信小程序的 web-view 中 const isMiniProgramWebview = isWechat && /miniProgram/i.test(navigator.userAgent); // 判断是否是 iPhone 设备 const isIPhone = /iPhone/i.test(navigator.userAgent); // 判断是否是顶级窗口(不是嵌套在 iframe 中)目前只有ios的浏览器中才会是顶级窗口 const isTopWindow = window.parent === window.self; return { isBrowser, // 是否在浏览器环境中 isWechat, // 是否在微信环境中 isMiniProgramWebview, // 是否在微信小程序的 web-view 中 isIPhone, // 是否是 iPhone 设备 isTopWindow, // 是否是顶级窗口 }; } /** * @description: 检查视频是否黑屏 * @param {Number} threshold 阈值,根据需要调整 默认20 * @return {Boolean} 返回是否为黑屏 */ function checkVideoBlackScreen(threshold = 20) { // 阈值,根据需要调整 // 获取视频元素 const video = document.getElementById('playerVideo'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; // 绘制视频帧到画布上 context.drawImage(video, 0, 0, canvas.width, canvas.height); // 获取图像数据 const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; let totalBrightness = 0; // 遍历图像数据,计算亮度 for (let i = 0; i < data.length; i += 4) { // 计算每个像素RGB(R:data[i], G:data[i + 1], B:data[i + 2])的亮度,这里使用简单的平均值 const brightness = (data[i] + data[i + 1] + data[i + 2]) / 3; // 累加亮度值 totalBrightness += brightness; } // 判断亮度是否低于阈值 const averageBrightness = totalBrightness / (data.length / 4); // 计算平均亮度 let isBlackScreen = averageBrightness < threshold; console.log('平均亮度', averageBrightness); // 释放资源 canvas.remove(); // 返回是否为黑屏 return isBlackScreen; } const { Dialog, Toast } = vant Toast.setDefaultOptions({ duration: 2000 }); // 从 CLOUD_GAME_SDK 结构中解构必要的函数和常量 const RtcEngineSDK = window.rtc_sdk.CloudGameSdk // 业务通道定时标识 let doConnectDirectivesIntervalerPing = null // 获取云机数据定时标识 let getUserCardInfoTimerInterval = null let getUserCardInfoRequestNum = 1 let doConnectDirectivesRequestNum = 1 let doConnectDirectivesTimerInterval = null // 触碰间隔定时标识 let noOperationSetTimeoutTimeInterval = null let noOperationSetIntervalTimeInterval = null // 倒计时定时标识 let countdownTimeInterval = null // 日志上报实例 let logReportObj = null; const app = new Vue({ el: '#app', data: { // 底部按钮 footerBtn: [{ key: 'task', value: 187, img: '../static/img/wx/gengduo_icon.png' }, { key: 'home', value: 3, img: '../static/img/wx/home_icon.png' }, { key: 'onBack', value: 4, img: '../static/img/wx/fanhui_icon.png' }], // 宽高 width: 0, height: 0, // webRtc实例 engine: {}, // 横竖屏幕 false是竖 isLandscape: false, // 悬浮球位置 levitatedSpherePositionData: {}, // 右侧弹窗 levitatedSphereVisible: false, // 清晰度数据 definitionList: [{ name: '自动', value: 2800 }, { name: '高清', value: 2800 }, { name: '标清', value: 1500, }, { name: '流畅', value: 1000, }], // 选中的清晰度 definitionValue: '标清', // 分辨率 resolutionRatioVisible: false, resolutionRatioList: [], // 需要用到的参数 parametersData: {}, // 屏幕分辨率 phoneSize: {}, // 业务指令通道实例 doConnectDirectivesWs: null, // 粘贴版 pasteVersionVisible: false, pasteVersionList: [], // 复制内容 copyTextValue: '', copyTextVisible: false, // 卡数据 userCardInfoData: {}, // 是否显示计时 timingVisible: false, // 计费规则 billingRulesVisible: false, // 应用推荐 applyRecommendVisible: false, // 是否是启动不操作自动退出云机功能 isFiringNoOperationSetTimeout: false, // 超过指定触碰时间的弹窗 noOperationSetTimeoutTimeVisible: false, // 超过指定触碰时间的弹窗文案 confirmButtonText: '', // 云机剩余时长 countdownTime: 0, // 是否支持webRTC isSupportRtc: !!( typeof RTCPeerConnection !== 'undefined' && typeof RTCIceCandidate !== 'undefined' && typeof RTCSessionDescription !== 'undefined' ), // 推荐列表 recommendList: [], layoutViewWidth: null, layoutViewHeight: null, // 是否显示video isShowVideo: false, plugFlowStartTime: null, }, created() { this.initConfig() }, mounted() { // 初始化日志上报 this.initLogReport(); // 获取卡信息,并连接云机拉流 this.getUserCardInfo(); }, // 销毁实例前 beforeDestroy() { console.log('销毁实例前'); }, computed: { // 右侧弹框退出相关按钮 exitList() { let arr = [{ name: '剪贴版', key: "shearplate", img: '../static/img/wx/jianqieban_icon.png' }, { name: '退出', key: 'signout', img: '../static/img/wx/tuichu_icon.png' }] if ([1, 2, 3].includes(+this.parametersData.userCardType)) { arr.push({ name: '退出并下机', key: 'dormant', img: '../static/img/wx/tuichu_icon.png' }) } return arr }, rtcMediaPlayerStyle() { let obj = { objectFit: "fill", width: `${this.layoutViewWidth}px`, height: `${this.layoutViewHeight}px`, } if (this.isLandscape) { obj = { width: `${this.layoutViewHeight}px`, height: `${this.layoutViewWidth}px`, transform: 'rotate(90deg)' } } return obj } }, methods: { // 初始化 initConfig() { // 获取窗口尺寸 this.getInitSize(); // 获取缓存悬浮球位置 let levitatedSpherePositionData = localStorage.getItem('levitatedSpherePositionData'); // 获取缓存清晰度 let definitionValue = localStorage.getItem('definitionValue') // 悬浮球位置 this.levitatedSpherePositionData = levitatedSpherePositionData ? JSON.parse(levitatedSpherePositionData) : { right: '15px', top: '15px' } // 清晰度 this.definitionValue = definitionValue ? definitionValue : '标清' // 获取参数 this.parametersData = getParameters() let { token, merchantSign } = this.parametersData // 给api增加需要的参数 request.defaults.headers.Authorization = token; request.defaults.headers.versionname = '5.9.1'; // 获取商户标识, 打开云机时必传, 用于日志上报 request.defaults.headers.merchantSign = merchantSign; window.onresize = () => { console.log('窗口尺寸变化'); this.getInitSize() } }, // 连接webRTC connectWebRtc() { const MediaSdk = window.rtc_sdk.MediaSdk; const videoRef = document.getElementById("videoRef"); this.plugFlowStartTime = +new Date() const { sn: topic, cardToken: authToken, internetIp, internetHttps, internetHttp, webrtcNetwork, webrtcTransferCmnet, webrtcTransferTelecom, webrtcTransferUnicom, videoCode } = this.userCardInfoData; const isWss = location.protocol === 'https:' const url = `${isWss ? 'wss://' : 'ws://'}${isWss ? internetHttps : internetHttp}/nats`; const ICEServerUrl = [ // 统一使用三网解析地址 { "CMNET": webrtcNetwork }, // 移动 { 'CHINANET-GD': webrtcNetwork }, // 电信 { 'UNICOM-GD': webrtcNetwork }, // 联通 ]; const connection = { mount: videoRef, displaySize: { // 整个页面的大小 width: this.layoutViewWidth, height: this.layoutViewHeight, }, topic, // SN号 必填 url, //信令服务地址 必填 ICEServerUrl, forwardServerAddress: '', // 转发服务器地址 ip: internetIp, // 实例ip controlToken: authToken, // 控制token width: 720, // 推流视频宽度 必填 height: 1280, // 推流视频高度 必填 cardWidth: 0, // 云机系统分辨率 宽 必填 cardHeight: 0, // 云机系统分辨率 高 必填 cardDensity: 0, // 云机系统显示 密度 必填 authToken, //拉流鉴权 token 必填 quality: '标清',// 画质(码率) 超清 | 高清 | 标清 | 极速 fps: 30, //必填 videoCodec: videoCode, // 视频编码格式 必填 videoCodecMethod: true, // 硬编true | 软编false isMuted: false, // 是否静音 isAllowedOpenCamera: true, // 是否允许打开摄像头 sendFollow: true, // 是否允许主控转发文本到实例 callback: (event) => { } } // 设置日志参数 推流质量 logReportObj.setParams({ imageQuality: 6000 }); // 6000在日志上报枚举中为高清 // 初始化 SDK this.engine = new MediaSdk(); console.log("初始化 SDK", this.engine); this.engine.RtcEngine(connection); // 监听回调方法 this.eventCallbackFunction(); }, // webRTC状态回调监听回调方法 eventCallbackFunction() { // TODO 查询屏幕方向未做 const engine = this.engine; // 连接成功 engine.on('CONNECT_SUCCESS', (r) => { console.log("webrtc连接成功====★★★★★", r); Toast.clear(); // 设置日志 推流状态为成功 let now = new Date(); logReportObj.setParams({ plugFowStatus: 1, linkWay: 1, timeConsuming: now.getTime() - logReportObj.timeStartTime, linkEndTime: logReportObj.formatDate(now) }); // 日志上报 logReportObj.collectLog(`推流成功`); if (r.code === 1005) { // 重连 if (this.getUserCardInfoRequestNum > 1) { engine.setControlAudio(true); // 设置音频 engine.changeTouchMode(true); // 重连成功后重置请求次数 this.getUserCardInfoRequestNum = 1; } else { engine.changeTouchMode(true); } } }); engine.on('CONNECT_CLOSE', (r) => { console.log("webrtc关闭====★★★★★", r); }); engine.on('CONNECT_ERROR', (r) => { console.log("webrtc异常状态====★★★★★", r); // 设置日志 推流状态为失败 logReportObj.setParams({ plugFowStatus: 2, linkWay: 0, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog( `webrtc异常状态 消息: ${JSON.stringify(r)}` ); Dialog.alert({ title: '提示', message: '链接超时', confirmButtonText: '确定', confirmButtonColor: '#3cc51f', beforeClose: (action, done) => { this.exit() done() } }) }); engine.on('keyboardFeedbackBean', (r) => { console.log("打开键盘事件====★★★★★", r); }); engine.on('RECEIVE_CHANNEL', (r) => { console.log("通道信息====★★★★★", r); }); engine.on('RECEIVE_ICE', (r) => { console.log("三网地址====★★★★★", r); }); engine.on('NETWORK_STATS', (r) => { // console.log("网络信息统计", r); }); engine.on('RECEIVE_BITRATE_STATUS', (r) => { // code 100251 成功 // code 100261 失败 // code 100262 输入码率值异常 // console.log("设置码率信息", r); }); // 解码状态 engine.on("DECODING_STATUS", (r) => { const statusMessages = { 10071: "rtc连接成功时间", 10072: "rtc连接失败时间", 10073: "鉴权成功时间", 1007: "渲染数据时间", 10074: "nats连接成功时间", 10075: "rtc连接失败时间", 10076: "rtc连接超时时间", 10077: "获取三网配置成功时间", 10078: "没有配置三网时间", 10079: "获取三网配置失败时间", 10080: "获取三网配置超时时间" }; const message = statusMessages[r.code] || `未知的状态码: ${r.code}`; console.log(`${message}:`, r); }); }, // 悬浮球click事件 onSphere(e) { this.levitatedSphereVisible = true e.preventDefault(); }, // 悬浮球按下事件 onSphereDown(e) { // 给元素设置鼠标按下状态 e.target.isMousedown = true; e.preventDefault(); }, // 悬浮球抬起事件 onSphereUp(e) { // 给元素设置鼠标按下状态 e.target.isMousedown = false; e.preventDefault(); }, // 悬浮球移动 touchmoveLevitatedSphere(e) { // 过滤未按下时的移动事件 if (e.type === 'mousemove' && !e.target.isMousedown) return let pageX, pageY; if (e.type === 'mousemove' && e.target.isMousedown) { pageX = e.pageX; pageY = e.pageY; } else if (e.type === 'touchmove') { // let { pageX, pageY } = e.targetTouches[0] pageX = e.targetTouches[0].pageX; pageY = e.targetTouches[0].pageY; } let min = 20 let MaxPageX = this.width - 20 let MaxPageY = this.height - 20 pageX = pageX <= min ? min : (pageX >= MaxPageX ? MaxPageX : pageX) pageY = pageY <= min ? min : (pageY >= MaxPageY ? MaxPageY : pageY) this.levitatedSpherePositionData = { left: `${pageX}px`, top: `${pageY}px`, transform: 'translate(-50%, -50%)' } e.preventDefault(); }, touchendLevitatedSphere(e) { localStorage.setItem('levitatedSpherePositionData', JSON.stringify(this.levitatedSpherePositionData)) }, // 清晰度 画质 definitionFun(item) { this.definitionValue = item.name const setBitrate = (item) => { const encoderConfig = { '自动': { width: 720, height: 1280, fps: 30 }, '高清': { width: 720, height: 1280, fps: 30 }, '标清': { width: 540, height: 960, fps: 25 }, '流畅': { width: 450, height: 800, fps: 20 } }; // 设置码率和是否允许自动 this.engine?.setCustomBitrate(item.value, item.name === '自动'); // 获取并设置编码器分辨率和帧率 const config = encoderConfig[item.name]; if (config) { this.engine?.setEncoderSize(config); } }; // 设置自动码率 setBitrate(item); // 设置日志参数 推流质量 logReportObj.setParams({ imageQuality: item.name }); localStorage.setItem('definitionValue', item.name) this.levitatedSphereVisible = false }, // 修改分辨率 resolutionRatio() { request.get('/api/resources/v5/machine/resolution/getResolvingPower', { params: { userCardId: this.parametersData.userCardId } }).then(res => { if (res.success) { this.resolutionRatioList = res.data.map(item => { item.height = item.high return item }) this.levitatedSphereVisible = false this.resolutionRatioVisible = true } }) }, // 确定修改分辨率 confirmResolution() { let { width, height, dpi: density } = this.phoneSize this.engine.makeResolution && this.engine.makeResolution({ width, height, density }) this.resolutionRatioVisible = false }, // 退出相关按钮操作 exitFun(key) { console.log(key) switch (key) { case 'dormant': Dialog.alert({ title: '提示', message: '确定退出云手机并下机', confirmButtonText: '确定', confirmButtonColor: '#3cc51f', showCancelButton: true, beforeClose: (action, done) => { if (action === 'cancel') done() if (action === 'confirm') { this.downline(done) } } }) break; case 'shearplate': this.copyTextValue = '' pasteText().then(content => { typeof content !== 'boolean' ? this.openPasteboard(content) : this.copyTextVisible = true }, err => { this.copyTextVisible = true }) break; case 'signout': this.exit() break; } }, // 业务指令 doConnectDirectives() { let { internetHttps, internetHttp, localIp, cardToken } = this.userCardInfoData const isWss = location.protocol === 'https:' let cUrl = `${isWss ? 'wss' : 'ws'}://${isWss ? internetHttps : internetHttp}/businessChannel?cardIp=${localIp}&token=${cardToken}&type=directives` this.doConnectDirectivesWs = new WebSocket(cUrl); this.doConnectDirectivesWs.binaryType = 'arraybuffer' clearInterval(doConnectDirectivesIntervalerPing) // 链接成功 this.doConnectDirectivesWs.onopen = (e) => { doConnectDirectivesIntervalerPing = setInterval(() => { if (this.doConnectDirectivesWs.readyState === 1) { this.doConnectDirectivesWs.send(JSON.stringify({ type: 'ping' })); } else { clearInterval(doConnectDirectivesIntervalerPing); } }, 3000) this.doConnectDirectivesWs.send(JSON.stringify({ type: 'getVsStatus' })) this.doConnectDirectivesWs.send(JSON.stringify({ type: 'bitRate', data: { bitRate: 1243000 } })) // 设置日志参数 推流质量 logReportObj.setParams({ imageQuality: 1243000 }); this.doConnectDirectivesWs.send(JSON.stringify({ type: 'InputMethod', data: { type: 2 } })) this.doConnectDirectivesWs.send(JSON.stringify({ type: 'getPhoneSize' })) } // 接受到的消息 this.doConnectDirectivesWs.onmessage = res => { const result = typeof res.data === 'string' ? JSON.parse(res.data) : res.data; switch (result.type) { // 分辨率 case 'getPhoneSize': case 'setPhoneSize': let data = JSON.parse(JSON.stringify(result.data)) let { width, height } = data if (width > height) { data.width = height data.height = width } this.phoneSize = data break // 云机复制过来的文本 case 'reProduceText': if (navigator.clipboard) { navigator.clipboard.writeText(result.data.text); } break case 'downAdnInstallRep': Toast(result.data.msg) break // 接受到这个消息就自动退出云机 case 'exitPhone': this.exit() break } } // 链接报错的回调 this.doConnectDirectivesWs.onerror = res => { // 设置日志 推流状态为失败 logReportObj.setParams({ plugFowStatus: 1, linkWay: 0, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog( `业务指令通道报错: url: ${res.target.url} type: ${res.type} 消息: ${JSON.stringify(res)}` ); clearInterval(doConnectDirectivesTimerInterval) if (doConnectDirectivesRequestNum > 6) { this.exit() return } doConnectDirectivesRequestNum++ this.doConnectDirectives() } }, // 粘贴版相关接口 shearContent({ type, params, queryStr }) { let url = '/api/public/v5/shear/content' if (queryStr) url += queryStr return request[type](url, params) }, // 清空全部、清除某条 deletePasteVersion(ids) { if (!ids) { Dialog.alert({ title: '提示', message: '确定清空剪贴板?', confirmButtonText: '确定', confirmButtonColor: '#3cc51f', showCancelButton: true, beforeClose: (action, done) => { if (action === 'cancel') done() if (action === 'confirm') { fun.bind(this)(done) } } }) return } fun.bind(this)() function fun(callBack = () => { }) { this.shearContent({ type: 'delete', queryStr: Qs.stringify( { ids: ids ? [ids] : this.pasteVersionList.map((v) => v.id), }, { arrayFormat: 'repeat', addQueryPrefix: true }, ) }).then(res => { if (res.status === 0) { this.getPasteVersion() callBack(true) } else { callBack(false) Toast(res.msg) } }).catch(() => { callBack(false) }) } }, // 获取粘贴版数据 getPasteVersion(callBack = () => { }) { this.shearContent({ type: 'get' }).then(res => { this.pasteVersionList = res.data callBack(true) }).catch(() => { callBack(false) }).finally(() => { }) }, // 复制弹窗是否关闭 beforeCloseCopy(action, done) { if (action !== 'confirm') { // 获取剪切板 this.getPasteVersion(() => { this.pasteVersionVisible = true this.levitatedSphereVisible = false done() }) return } if (!this.copyTextValue) { Toast('请输入复制到剪切板的内容') done(false) return } this.openPasteboard(this.copyTextValue, done) }, // 打开粘贴板 async openPasteboard(content, callBack = () => { }) { this.shearContent({ type: 'post', params: { content } }).then().finally(() => { callBack() // 获取剪切板 this.getPasteVersion(() => { this.pasteVersionVisible = true this.levitatedSphereVisible = false }) }) }, // 复制粘贴某条数据 copyPasteVersiontext(e) { clickCopyText(e, (event) => { this.doConnectDirectivesWs.send(JSON.stringify({ type: 'cutting', data: { str: event.text, }, })) Toast('复制成功') }, () => { Toast('复制失败') }) }, // 获取卡信息 getUserCardInfo() { Toast.loading({ duration: 0, // 持续展示 toast message: '数据加载中...', }); // 从url中获取userCardId let { userCardId } = this.parametersData userCardId = +userCardId const statusTips = { // 5200:RBD资源挂载中 5200: '网络异常,请稍后重试', // 入使用排队9.9,年卡 5220: '云手机正在一键修复中', 5203: '正在排队中,请稍等', // 9.9年卡连接异常,重新进入排队 5204: '云机异常,正在为你重新分配云机', 5228: '卡的网络状态为差', 5229: '接口返回链接信息缺失', } let { isWeixin } = this.parametersData; let clientType = +isWeixin ? 'wx' : undefined; // 设置上报参数 logReportObj.setParams({ userCardId }); clientType && logReportObj.setParams({ clientType }); // 获取卡信息 request.post('/api/resources/user/cloud/connect', { userCardId }).then(async res => { try { if (!res.success) { // 设置日志 推流状态为失败,和链接状态 logReportObj.setParams({ plugFowStatus: 2, linkWay: logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog( `接口获取数据失败: url: /api/resources/user/cloud/connect method: post 参数: ${JSON.stringify({ userCardId })} 响应: ${JSON.stringify(res)}` ); return; } const data = res.data; // 设置上报参数 logReportObj.setParams({ videoType: data.videoCode.toLowerCase(), resourceId: data.resourceId }); switch (res.status) { case 0: getUserCardInfoRequestNum = 1 // 不支持webRTC跳转到指定的页面 if (!res.data.isWebrtc) { // 关闭日志上报 logReportObj.destroy(); // 跳转指定页面 location.replace(`${location.origin}/h5/webRtcYJ/WXtrialInterface.html${location.search}`) return } if (!this.isSupportRtc) { // 设置日志 推流状态为失败 logReportObj.setParams({ plugFowStatus: 2, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog(`${+isWeixin ? '微信小程序' : ''}当前版本暂不支持使用`); Dialog.alert({ title: '提示', message: `${+isWeixin ? '微信小程序' : ''}当前版本暂不支持使用,可下载谷歌浏览器或客户端进行使用`, confirmButtonText: '确定', confirmButtonColor: '#3cc51f', beforeClose: (action, done) => { this.exit() done() } }) return } // webRtc连接,需获取连接中转地址 if (res.data.webrtcNetworkAnalysis) { // 如果有网络分析的请求地址, 则请求,则否失败 const webrtcNetworkAnalysisReq = await request.get(res.data.webrtcNetworkAnalysis); if (webrtcNetworkAnalysisReq !== null && webrtcNetworkAnalysisReq.success) { if (webrtcNetworkAnalysisReq.data) { // 保存获取的连接地址到上个请求的响应中, 方便后面使用 res.data.webrtcNetwork = webrtcNetworkAnalysisReq.data; // 设置上报参数 logReportObj.setParams({ transferServerIp: webrtcNetworkAnalysisReq.data }); } else { // 设置上报参数 logReportObj.setParams({ linkWay: 4, plugFowStatus: 2 }); // 日志上报 logReportObj.collectLog( `webRtc连接,获取中转地址成功,但返回数据为空: url: ${res.data.webrtcNetworkAnalysis} method: get 参数: 无 响应: ${JSON.stringify(webrtcNetworkAnalysisReq)}` ); } } else { // 设置上报参数 logReportObj.setParams({ linkWay: 4, plugFowStatus: 2, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog( `webRtc连接,获取中转地址失败: url: ${res.data.webrtcNetworkAnalysis} method: get 参数: 无 响应: ${JSON.stringify(webrtcNetworkAnalysisReq)}` ); } } else { // 设置上报参数 logReportObj.setParams({ linkWay: 4, plugFowStatus: 2 }); // 日志上报 logReportObj.collectLog( `webRtc连接,获取请求中转地址为空: url: /api/resources/user/cloud/connect method: post 参数: ${JSON.stringify({ userCardId })} 响应: ${JSON.stringify(res)}` ); } // 如果没有获取到连接地址, 则失败 if (!res.data.webrtcNetwork) { Dialog.alert({ title: '提示', message: '访问失败,请稍后重试', confirmButtonText: '确定', confirmButtonColor: '#3cc51f', beforeClose: (action, done) => { this.exit() done() } }) return; } this.userCardInfoData = res.data // 进入连接云机准备阶段 this.connectWebRtc(); return case 5200: case 5220: case 5203: case 5204: Toast(statusTips[res.status]); // 设置上报参数 logReportObj.setParams({ linkWay: logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog(statusTips[res.status] || `webRtc连接,获取请求中转地址返回异常: url: /api/resources/user/cloud/connect method: post 参数: ${JSON.stringify({ userCardId })} 响应: ${JSON.stringify(res)}` ); break default: // 设置上报参数 logReportObj.setParams({ linkWay: logReportObj.RESPONSE_CODE[res.status] || 0, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog(statusTips[res.status] || `webRtc连接,获取请求中转地址返回异常: url: /api/resources/user/cloud/connect method: post 参数: ${JSON.stringify({ userCardId })} 响应: ${JSON.stringify(res)}` ); Toast('画面异常,请重新进入') break } setTimeout(() => { this.exit() }, 3000) } catch (error) { } }).catch((error) => { // 设置上报参数 logReportObj.setParams({ linkWay: 4, plugFowStatus: 2, linkEndTime: logReportObj.formatDate(new Date()) }); // 日志上报 logReportObj.collectLog( `接口获取数据失败: url: /api/resources/user/cloud/connect method: post 参数: ${JSON.stringify({ userCardId })} 响应: ${JSON.stringify(error)}` ); }) // 重连 function reconnect() { // 重连6次结束 if (getUserCardInfoRequestNum > 6) { Toast('网络异常,请稍后重试') clearTimeout(getUserCardInfoTimerInterval) setTimeout(() => { this.exit() }, 3000) return } // 重连 getUserCardInfoTimerInterval = setTimeout(() => { this.getUserCardInfo() getUserCardInfoRequestNum++ }, 3000) } }, // 超过指定触碰时间是否提示关闭链接 pushflowPopup() { request.get('/api/public/v5/pushflow/popup').then(res => { if (res.success) { this.isFiringNoOperationSetTimeout = res.data this.noOperationSetTimeout() } }) }, // 退出功能 exit() { // 关闭日志上报 logReportObj.destroy(); this.engine.disconnect && this.engine.disconnect(); this.doConnectDirectivesWs && this.doConnectDirectivesWs.close() uni.reLaunch({ url: '/pages/index/index' }); // 上面的方法执行未生效就走这里 if (window.history.length > 1) { window.history.back(); } }, // 不触碰屏幕显示退出链接弹窗 noOperationSetTimeout(key) { if (!this.isFiringNoOperationSetTimeout) return clearTimeout(noOperationSetTimeoutTimeInterval) if (key === 'cancel') { clearInterval(noOperationSetIntervalTimeInterval) this.noOperationSetTimeoutTimeVisible = false this.noOperationSetTimeout() return } noOperationSetTimeoutTimeInterval = setTimeout(() => { let index = 9 this.confirmButtonText = '退出(10秒)' this.noOperationSetTimeoutTimeVisible = true noOperationSetIntervalTimeInterval = setInterval(() => { this.confirmButtonText = `退出${index ? `(${index}秒)` : ''}` index-- if (index < 0) { this.noOperationSetTimeout('cancel') this.exit() } }, 1000) }, 300000) }, // 获取云机剩余时长 async getResidueTime() { clearInterval(countdownTimeInterval) const { userCardType, isShowCountdown, isShowRule } = this.parametersData const { userCardId } = this.userCardInfoData if (![1, 2, 3].includes(+userCardType)) return const res = await request.get(`/api/resources/yearMember/getResidueTime?userCardId=${userCardId}`) let time = res.data; if (!res.status) { this.countdownTime = residueTimeStamp(time) await request.get(`/api/resources/yearMember/startTime?userCardId=${userCardId}`) if (+isShowCountdown) this.timingVisible = true if (+isShowRule) this.billingRulesVisible = true countdownTimeInterval = setInterval(() => { if (time <= 0) { clearInterval(countdownTimeInterval) this.downline() return } time-- this.countdownTime = residueTimeStamp(time) }, 1000) } }, // 关闭倒计时弹窗 handlecountdownTimeClose() { const { userCardId } = this.userCardInfoData request.get(`/api/resources/yearMember/closeRemind?userCardId=${userCardId}`).then(res => { if (!res.status) { clearInterval(countdownTimeInterval) this.timingVisible = false return } Toast(res.msg); }) }, // 退出并下机 downline(fun = () => { }) { const { userCardId } = this.userCardInfoData request.get(`/api/resources/yearMember/downline?userCardId=${userCardId}`).then(res => { if (!res.status) { fun(true) // 通信给h5项目告知是退出并下机 parent.postMessage( { type: 'exit', }, '*', ); uni.postMessage({ data: { type: 'exit' } }); this.exit() return } fun(false) Toast(res.msg); }) }, // 获取推荐列表 getRecommend() { const { userCardId } = this.userCardInfoData request.get(`/api/public/v1/market/get/recommend?userCardId=${userCardId}`).then(res => { if (!res.status) { this.billingRulesVisible = false this.recommendList = res.data this.recommendList.length && (this.applyRecommendVisible = true) } }) }, // 下载apk downAndInstallApk({ downloadUrl: apkUrl, id: taskUid }) { this.doConnectDirectivesWs.send(JSON.stringify({ type: 'downAndInstallApk', data: { apkUrl, taskUid }, })) }, // 返回、主页、任务器 footerBtnFun(key) { this.engine && this.engine.sendKey(key) this.noOperationSetTimeout() }, // 获取初始化尺寸 getInitSize() { // 高度、悬浮球相关配置 this.height = window.innerHeight; this.width = window.innerWidth; this.$nextTick(() => { // 云机画面宽高 let layoutView = document.querySelector('.layout-view') // 获取视口宽度,webRTC需要做成16:9的画面 let currentWidth = layoutView.clientWidth; let currentHeight = layoutView.clientHeight; // 计算当前视口的宽高比 const currentRatio = currentWidth / currentHeight; // 9:16 的目标比例 const targetRatio = 9 / 16; console.log(`当前视口的宽高比: ${currentRatio}`); // 判断当前视口的宽高比与目标比例的关系 if (currentRatio > targetRatio) { // 当前视口的宽高比大于目标比例,说明宽度“过宽”,需要以高度为基准 console.log("当前视口宽度过宽,应以高度为基准调整宽度"); this.layoutViewWidth = currentHeight * targetRatio; this.layoutViewHeight = currentHeight; console.log(`1目标: 宽${this.layoutViewWidth},高${this.layoutViewHeight}`); } else { // 当前视口的宽高比小于目标比例,说明高度“过高”,需要以宽度为基准 console.log("当前视口高度过高,应以宽度为基准调整高度"); this.layoutViewHeight = currentWidth / targetRatio; this.layoutViewWidth = currentWidth; console.log(`2目标: 宽${this.layoutViewWidth},高${this.layoutViewHeight}`); } // 悬浮球位置设置为默认位置 this.levitatedSpherePositionData = { right: '15px', top: '15px' } }) }, // 音量 volumeControl(value) { this.engine.sendKey && this.engine.sendKey(value) this.$refs.rtcMediaPlayer && (this.$refs.rtcMediaPlayer.muted = false) }, // 初始化日志上报实例 initLogReport() { // 初始化日志上报实例 logReportObj = new logReport({ request }); uni.getEnv((res) => { // 设置上报参数 logReportObj.setParams({ clientType: Object.keys(res)[0] }); }) } } }) // 播放按钮点击事件 function playOnBtn() { const { isTips } = this.parametersData; Dialog.alert({ title: '提示', message: `${+isTips ? '开始' : '继续'}使用云手机`, confirmButtonText: '确定', confirmButtonColor: '#3cc51f', beforeClose: (action, done) => { if (action === 'confirm') { this.isShowVideo = true Toast.clear(); this.$refs.rtcMediaPlayer.play() setTimeout(() => { this.doConnectDirectives() this.pushflowPopup() this.getResidueTime() done() }) } } }); } // 获取URL参数 function getParameters() { let arr = location.search.split('?') let obj = {} if (arr[1]) { arr = arr[1].split('&') arr.forEach(item => { let [key, value = ''] = item.split('=') obj[key] = value }) } return obj } // 倒计时处理的时间 function residueTimeStamp(value) { let theTime = value;//秒 let middle = 0;//分 let hour = 0;//小时 if (theTime > 59) { middle = parseInt(theTime / 60); theTime = parseInt(theTime % 60); } if (middle > 59) { hour = parseInt(middle / 60); middle = parseInt(middle % 60); } theTime < 10 ? theTime = '0' + theTime : theTime = theTime middle < 10 ? middle = '0' + middle : middle = middle hour < 10 ? hour = '0' + hour : hour = hour return hour + ':' + middle + ':' + theTime }