t_finder месяцев назад: 8
Родитель
Сommit
795b298f0c
4 измененных файлов с 551 добавлено и 0 удалено
  1. 19 0
      layouts/cloudPhone.vue
  2. 36 0
      pages/rtcEngine/config/meta.js
  3. 8 0
      pages/rtcEngine/config/request.js
  4. 488 0
      pages/rtcEngine/rtc.vue

+ 19 - 0
layouts/cloudPhone.vue

@@ -0,0 +1,19 @@
+<template>
+    <v-main ref="main">
+      <Nuxt />
+    </v-main>
+</template>
+
+<script>
+// 云手机拉流页面
+export default {
+  name: 'CloudPhone',
+  data() {
+    return {};
+  },
+  created() {},
+  mounted() {
+    this.$vuetify.cloudPhoneMain = this.$refs.main;
+  },
+};
+</script>

+ 36 - 0
pages/rtcEngine/config/meta.js

@@ -0,0 +1,36 @@
+/**
+ * @description: 页面meta 标签配置
+*/
+export default [
+  { hid: 'description', name: 'description', content: 'webRTC' },
+  // 告诉 IE 浏览器使用最新的渲染引擎(Edge 模式)来渲染页面,而不是使用旧的兼容模式。这可以确保页面在 IE 浏览器中以最佳方式显示。
+  { hid: 'X-UA-Compatible', httpEquiv: 'X-UA-Compatible', content: 'IE=edge' },
+  // 强制页面在移动设备上以竖屏模式显示
+  { hid: 'x5-orientation', name: 'x5-orientation', content: 'portrait' },
+  // 与 x5-orientation 类似,用于指定页面的方向
+  { hid: 'screen-orientation', name: 'screen-orientation', content: 'portrait' },
+  // 控制页面在移动设备上的视口(viewport)行为
+  { hid: 'viewport', name: 'viewport', content: 'width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no,viewport-fit=cover' },
+  // 允许网页应用以全屏模式运行 当用户将网页添加到主屏幕后,点击图标打开时,页面会以全屏模式显示,隐藏浏览器的地址栏和导航栏。适用于将网页应用伪装成原生应用的场景
+  { hid: 'apple-mobile-web-app-capable', name: 'apple-mobile-web-app-capable', content: 'yes' },
+  // 控制页面在 iOS 设备上的全屏模式和状态栏颜色
+  { hid: 'apple-mobile-web-app-status-bar-style', name: 'apple-mobile-web-app-status-bar-style', content: 'black' },
+  // 禁止自动识别电话号码和邮箱地址
+  { hid: 'format-detection', name: 'format-detection', content: 'telphone=no, email=no' },
+  // 启用360浏览器的极速模式
+  { hid: 'renderer', name: 'renderer', content: 'webkit' },
+  // 针对手持设备优化
+  { hid: 'HandheldFriendly', name: 'HandheldFriendly', content: 'true' },
+  // 微软的老式浏览器优化
+  { hid: 'MobileOptimized', name: 'MobileOptimized', content: '320' },
+  // UC强制全屏
+  { hid: 'full-screen', name: 'full-screen', content: 'yes' },
+  // QQ强制全屏
+  { hid: 'x5-fullscreen', name: 'x5-fullscreen', content: 'true' },
+  // UC应用模式
+  { hid: 'browsermode', name: 'browsermode', content: 'application' },
+  // QQ应用模式
+  { hid: 'x5-page-mode', name: 'x5-page-mode', content: 'app' },
+  // 禁用 Windows Phone 8 及更高版本中的灰色点击高亮
+  { hid: 'msapplication-tap-highlight', name: 'msapplication-tap-highlight', content: 'no' },
+]

+ 8 - 0
pages/rtcEngine/config/request.js

@@ -0,0 +1,8 @@
+/**
+ * @description: 单独封装请求
+ * 用于响应的数据结构不一致, 或是是其他平台的接口
+*/
+import axios from 'axios';
+const request = axios.create({});
+
+export default request;

+ 488 - 0
pages/rtcEngine/rtc.vue

@@ -0,0 +1,488 @@
+<template>
+  <div align="center" class="rtc-page cover-bg" :style="{height: pageData.height + 'px'}">
+    <!-- video 容器 -->
+    <div class="video-wrapper" id="videoRef" :style="{width: pageData.videoWidth + 'px', height: pageData.videoHeight + 'px'}"></div>
+    <!-- 三menu键 -->
+    <div id="foot-menu-wrap">
+      <div @click.stop=sendKey(187)><van-icon name="wap-nav" size="1rem"/></div>
+      <div @click.stop=sendKey(3)><van-icon name="wap-home-o" size="1rem"/></div>
+      <div @click.stop=sendKey(4)><van-icon name="arrow-left" size="1rem"/></div>
+    </div>
+  </div>
+</template>
+
+<script>
+import meta from './config/meta.js';
+import request from './config/request.js';
+
+/**
+ * @description: 判断当前页面运行环境
+ * @return {Object} 返回当前页面运行环境
+ */
+ const isBrowserEnvironment = function() {
+  // 判断是否在浏览器环境中
+  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, // 是否是顶级窗口
+  };
+}
+
+export default {
+  auth: false,
+  name: 'webRTC',
+  layout: 'cloudPhone',
+  head() {
+    return {
+      title: '云手机',
+      meta: [ ...meta ],
+      script: [
+        {
+          // js文件指向 static 目录下的文件
+          src: './rtcEngine/config/js/SDK.min.js', // sdk 2.0文件
+          type: 'text/javascript',
+          async: false,
+          defer: false,
+          onload: '$_script_loadHandler()', // 加载成功回调created生命周期中定义的方法
+          onerror: '$_script_errHandler()', // 加载失败回调created生命周期中定义的方法
+        }
+      ]
+    }
+  },
+  data() {
+    return {
+      // SDK加载状态
+      sdkLoadStatus: 'loading', // sdk 加载状态 [loading|success|error]
+      // 页面数据
+      pageData: {
+        width: 0, // 页面宽度
+        height: 0, // 页面高度
+        footMenuHeight: 0, // 底部菜单高度
+        videoWidth: 0, // 视频宽度
+        videoHeight: 0, // 视频高度
+      },
+      // 是否支持webRTC
+      isSupportRtc: !!(
+        typeof RTCPeerConnection !== 'undefined' &&
+        typeof RTCIceCandidate !== 'undefined' &&
+        typeof RTCSessionDescription !== 'undefined'
+      ),
+      // url 问号后的参数
+      parametersData: {},
+      // 卡的连接信息
+      connectData: {},
+      // 云手机引擎 播放器实例
+      engine: null, 
+    }
+  },
+  // 页面初始化后触发
+  async fetch() {
+    // 预生产环境
+    // http://192.168.211.37:3000/h5/rtcEngine/rtc/?record=2990744&userCardId=2990744&mealType=VIP&sourceType=0&userCardType=0&validTime=43200&rm=preZJHZ&authPhone=none&username=CGJkh1646034892SZX&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyYW5kb20iOiI5MTY4OCIsImNsaWVudCI6IjciLCJ1c2VyVHlwZSI6IjIiLCJtZXJjaGFudFNpZ24iOiJTWlgiLCJleHAiOjE3NDc1MjExMTIsInVzZXJuYW1lIjoiQ0dKa2gxNjQ2MDM0ODkyU1pYIn0.zqRNm0uW79VX2KV0sv3r9OsXHFOzkIpZP_tem4-lL4M&isTips=1&isWeixin=0&merchantSign=SZX
+    // 测试环境
+    // http://192.168.211.37:3000/h5/rtcEngine/rtc/?record=902481&userCardId=902481&mealType=sq&sourceType=0&userCardType=0&validTime=10334&rm=tencent-ap-tianjin-1&authPhone=none&username=0EsH01666175530SZX&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyYW5kb20iOiI0MTg5MyIsImNsaWVudCI6IjciLCJ1c2VyVHlwZSI6IjIiLCJtZXJjaGFudFNpZ24iOiJTWlgiLCJleHAiOjE3NDczODQ2MTgsInVzZXJuYW1lIjoiMEVzSDAxNjY2MTc1NTMwU1pYIn0.fTXawDu4SyEj4mIGr61ZXt0RSXzt3JztPq8-rFe-Zes&isTips=1&isWeixin=0&merchantSign=SZX
+    // 获取页面传递参数
+    this.parametersData = this.$route.query;
+    // 调用接口获取卡连接数据
+    const cardData = await this.getConnectData(this.parametersData);
+    
+    // 判断卡的连接方式
+    const connectData = await this.judgeConnectType(cardData);
+
+    // 保存卡连接信息
+    this.connectData = connectData;
+  },
+  computed: {
+    // 是否为微信浏览器环境
+    isWeChatBrowser() {
+      return this.$userAgent.isWx;
+    }
+  },
+  created() {
+    this.$toast.loading({
+        duration: 0, // 持续展示 toast
+        message: '数据加载中...',
+    });
+    // 设置html标签的背景为黑色
+    document.body.style.background = '#000';
+
+    // 定义全局变量 用于监听sdk加载状态
+    window.$_script_loadHandler = ()=> {
+      console.log('SDK加载成功');
+      this.sdkLoadStatus = 'success';
+    };
+    window.$_script_errHandler = ()=> {
+      console.log('SDK加载失败');
+      this.sdkLoadStatus = 'error';
+    }
+  },
+  mounted() {
+    // 获取窗口尺寸
+    this.getInitSize();
+    window.onresize = () => {
+        console.log('窗口尺寸变化');
+        this.getInitSize()
+    };
+    // 初始化页面监听事件
+    this.initListener();
+
+    // 定时器 监听sdk加载状态, 加载成功和失败则停止定时器, 加载中则继续监听
+    let timer = setInterval(() => {
+      console.log('SDK加载状态:', this.sdkLoadStatus);
+      if (this.sdkLoadStatus === 'success' || window?.rtc_sdk?.MediaSdk) {
+        this.sdkLoadStatus = 'success';
+        console.log('SDK加载成功');
+        clearInterval(timer);
+        // 初始化webRTC
+        this.initWebRtc();
+      }
+      if (this.sdkLoadStatus === 'error') {
+        console.log('SDK加载失败');
+        clearInterval(timer);
+      }
+    })
+  },
+  // 页面销毁前触发
+  beforeDestroy() {
+    // 销毁页面监听事件
+    this.destroyListener();
+  },
+  methods: {
+    // 初始化页面监听事件
+    initListener() {
+      // 禁止双击缩放
+      document.addEventListener('dblclick', this.preventDefault);
+      // 添加监听 页面显示或隐藏 事件
+      document.addEventListener('visibilitychange', this.visibilitychanged);
+    },
+    // 销毁页面监听事件
+    destroyListener() {
+      // 允许双击缩放
+      document.removeEventListener('dblclick', this.preventDefault);
+      // 移除监听 页面显示或隐藏 事件
+      document.removeEventListener('visibilitychange', this.visibilitychanged);
+    },
+    // 阻止默认事件
+    preventDefault(e) {
+      e.preventDefault();
+    },
+    // 监听 页面显示或隐藏 执行的函数
+    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', this.visibilitychanged);
+      }
+    },
+    // 获取初始化尺寸
+    getInitSize() {
+      // 获取窗口尺寸
+      this.pageData.height = window.innerHeight;
+      this.pageData.width = window.innerWidth;
+      // 获取底部菜单高度
+      let {clientHeight: h} = document.getElementById("foot-menu-wrap");
+      this.pageData.footMenuHeight = h;
+
+      // 计算视频尺寸 webRTC需要做成16:9的画面
+      let videoWidth = this.pageData.width;
+      let videoHeight = this.pageData.height - this.pageData.footMenuHeight;
+
+      // 计算当前视口的宽高比
+      const currentRatio = videoWidth / videoHeight;
+      console.log(`当前视口的宽高比: ${currentRatio}`);
+
+      // 9:16 的目标比例
+      const targetRatio = 9 / 16;
+
+      // 判断当前视口的宽高比与目标比例的关系
+      if (currentRatio > targetRatio) {
+        // 当前视口的宽高比大于目标比例,说明宽度“过宽”,需要以高度为基准
+        console.log("当前视口宽度过宽,应以高度为基准调整宽度");
+        this.pageData.videoWidth = videoHeight * targetRatio;
+        this.pageData.videoHeight = videoHeight;
+        console.log(`1目标: 宽${this.pageData.videoWidth},高${this.pageData.videoHeight}`);
+      } else {
+        // 当前视口的宽高比小于目标比例,说明高度“过高”,需要以宽度为基准
+        console.log("当前视口高度过高,应以宽度为基准调整高度");
+        this.pageData.videoWidth = videoWidth / targetRatio;
+        this.pageData.videoHeight = videoWidth;
+        console.log(`2目标: 宽${this.pageData.videoWidth},高${this.pageData.videoHeight}`);
+      }
+    },
+    // 获取卡的信息
+    async getConnectData(params) {
+      try {
+        const res = await this.$axios.$post('/resources/user/cloud/connect', { userCardId: params. userCardId}, {
+          headers: {
+            merchantSign: params.merchantSign,
+          },
+        });
+        if (!res.success) {
+          // TODO 提示错误信息 和 日志参数及上报
+          // res.status状态码枚举值 0: 正常
+          const statusEnum = {
+            // 5200:RBD资源挂载中
+            5200: '网络异常,请稍后重试',
+            // 入使用排队9.9,年卡
+            5220: '云手机正在一键修复中',
+            5203: '正在排队中,请稍等',
+            // 9.9年卡连接异常,重新进入排队
+            5204: '云机异常,正在为你重新分配云机',
+            5228: '卡的网络状态为差',
+            5229: '接口返回链接信息缺失',
+          }
+          // 提示错误信息
+          this.$toast(statusEnum[res.status] || '网络异常,请稍后重试')
+          return Promise.reject(new Error(statusEnum[res.status] || '网络异常,请稍后重试'));
+        }
+        return res.data;
+      }catch (error) {
+        console.log('error connectAxios:', error);
+        return Promise.reject(error);
+      }
+    },
+    // 判断卡的连接方式
+    async judgeConnectType(cardData) {
+      try {
+        // 不支持webRTC跳转到指定的页面进行拉流
+        if (!cardData.isWebrtc) {
+            // 跳转指定页面
+            location.replace(`${location.origin}/h5/webRtcYJ/WXtrialInterface.html${location.search}`)
+            return Promise.reject();
+        }
+
+        // 是否支持webRTC
+        if (!this.isSupportRtc) {
+          // TODO 提示错误信息 和 日志参数及上报
+          this.$dialog.alert({
+            title: '提示',
+            message: '当前环境不支持使用,可下载谷歌浏览器或客户端进行使用',
+            confirmButtonText: '确定',
+            confirmButtonColor: '#3cc51f',
+            callback: () => {
+              // TODO 关闭页面
+            }
+          })
+          return Promise.reject(new Error('当前浏览器不支持webRTC'));
+        }
+
+        // webRtc连接,需获取连接中转地址
+        if (cardData.webrtcNetworkAnalysis) {
+          // 如果有网络分析的请求地址, 则请求,否则失败
+          const { data: webrtcNetworkAnalysisReq }= await request.get(cardData.webrtcNetworkAnalysis); // 这个接口单独使用axios请求, 因为返回的数据跟封装的数据结构不一统一,是其他平台的接口,所以单独请求
+
+          if (webrtcNetworkAnalysisReq !== null && webrtcNetworkAnalysisReq.success && webrtcNetworkAnalysisReq.data) {
+              // 保存获取的连接地址到connect的请求的响应中, 方便后面使用
+              cardData.webrtcNetwork = webrtcNetworkAnalysisReq.data;
+              return cardData;
+          }else{
+            // TODO 提示错误信息 和 日志参数及上报
+            return Promise.reject(new Error('网络分析请求失败'));
+          }
+        }else{
+          // TODO 提示错误信息 和 日志参数及上报
+          return Promise.reject(new Error('网络分析请求地址不存在'));
+        }
+      } catch (error) {
+        console.log('判断卡的连接方式', error);
+        return Promise.reject(error);
+      }
+    },
+    // 初始化webRTC及相关配置
+    initWebRtc() {
+      // 检查连接卡信息是否存在,不存在则循环等待,直到存在为止
+      if (!Object.keys(this.connectData).length) {
+        setTimeout(() => {
+          this.initWebRtc();
+        }, 50);
+        return;
+      }
+
+      // 获取挂载的容器元素
+      const videoRef = document.getElementById("videoRef");
+
+      // 解构connectData中的数据
+      const { sn: topic, cardToken: authToken, localIp, internetHttps, internetHttp, webrtcNetwork, webrtcTransferCmnet, webrtcTransferTelecom, webrtcTransferUnicom, videoCode } = this.connectData;
+      // 判断长连接的协议方式
+      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.pageData.videoWidth,
+            height: this.pageData.videoHeight,
+        },
+        topic, // SN号  必填
+        url,  //信令服务地址  必填
+        ICEServerUrl,
+        forwardServerAddress: '', // 转发服务器地址
+        ip: localIp, // 实例ip
+        controlToken: '', // 控制token
+        width: 720, // 推流视频宽度  必填
+        height: 1080,  // 推流视频高度  必填
+        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)=> {}
+      };
+
+      // 获取SDK类
+      const MediaSdk = window.rtc_sdk.MediaSdk;
+      // 初始化 SDK
+      this.engine = new MediaSdk();
+      console.log('RtcEngineConfig==', connection)
+      // 初始化 SDK 并传入连接参数
+      this.engine.RtcEngine(connection);
+
+      // 监听回调方法
+      this.eventCallbackFunction();
+    },
+    // webRTC状态回调监听回调方法
+    eventCallbackFunction() {
+      const engine = this.engine;
+
+      // 连接成功
+      engine.on('CONNECT_SUCCESS', (r) => {
+        console.log("webrtc连接成功====★★★★★", r);
+        if (r.code === 1005) { // 1005: 拉流鉴权成功
+          this.$toast.clear();
+          // 播放视频
+          setTimeout(() => {
+            engine.mediaElement.node.play();
+          }, 50)
+        }
+      });
+
+      // 连接关闭
+      engine.on('CONNECT_CLOSE', (r) => {
+          console.log("webrtc关闭====★★★★★", r);
+      });
+
+      // 连接异常
+      engine.on('CONNECT_ERROR', (r) => {
+        console.log("webrtc异常状态====★★★★★", r);
+      });
+    },
+    // 三键
+    sendKey (keyCode) {
+      console.log('sendKey', keyCode);
+      this.engine.sendKey(keyCode);
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+html{
+  background-color: #000;
+}
+// 动态生成 从 0 到 100px 的样式
+@for $i from 0 through 100 {
+  .mb-#{$i} {
+    margin-bottom: #{$i}px;
+  }
+
+  .mt-#{$i} {
+    margin-top: #{$i}px;
+  }
+
+  .ml-#{$i} {
+    margin-left: #{$i}px;
+  }
+
+  .mr-#{$i} {
+    margin-right: #{$i}px;
+  }
+}
+$-radeus-12: 12px;
+$-bg-yellow: rgb(255, 253, 241);
+
+.rtc-page{
+  position: relative;
+  font-size: 14px;
+  
+  .video-wrapper{
+    position: relative;
+  }
+}
+
+.cover-bg{
+  background-color: #000;
+}
+
+#foot-menu-wrap{
+  border-width: 0px;
+  position: absolute;
+  left: 0px;
+  bottom: 0px;
+  width: 100%;
+  height: 40px;
+  background: inherit;
+  background-color: rgba(0, 12, 23, 1);
+  border: none;
+  border-radius: 0px;
+  -moz-box-shadow: none;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+  z-index: 5;
+  display: flex;
+  justify-content: space-evenly;
+  align-items: center;
+  color: #fff;
+
+  // .foot-menu{
+  //   width: 40px;
+  // }
+}
+</style>