rtc.vue 34 KB

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