Parcourir la source

feat(disk): disk组件

zengzhixiang il y a 2 ans
Parent
commit
587d817e45
4 fichiers modifiés avec 658 ajouts et 155 suppressions
  1. 0 155
      components/disk.vue
  2. 199 0
      components/disk/index.vue
  3. 122 0
      components/disk/touch.vue
  4. 337 0
      components/disk/video.vue

+ 0 - 155
components/disk.vue

@@ -1,155 +0,0 @@
-<template>
-  <div
-    :data-id="diskInfo && diskInfo.id"
-    class="disk reverse flex flex-col h-full"
-  >
-    <header class="disk-header"></header>
-    <main class="disk-main flex-auto"></main>
-    <footer class="disk-footer h-50rpx"></footer>
-    <v-overlay absolute :value="loading">
-      <div class="flex flex-col justify-center items-center">
-        <v-progress-circular indeterminate class="w-32rpx h-32rpx"></v-progress-circular>
-        <div v-if="loadingLabel" class="label mt-2">{{ loadingLabel }}</div>
-      </div>
-    </v-overlay>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'Disk',
-  props: {
-    userCardId: {
-      type: Number,
-      default: null,
-    },
-  },
-  data() {
-    return {
-      diskInfo: null,
-      connectInfo: null,
-      loading: false,
-      loadingLabel: null,
-    };
-  },
-  async fetch() {
-    // await this.getDiskInfo();
-  },
-  watch: {
-    userCardId: {
-      async handler(userCardId) {
-        if (userCardId) {
-          try {
-            this.loading = true;
-            await this.getDiskInfo(userCardId);
-            await this.connect(userCardId);
-          } finally {
-            this.loading = false;
-          }
-        }
-      },
-      immediate: true,
-    },
-  },
-  methods: {
-    // 查询云机
-    async getDiskInfo(userCardId) {
-      const res = await this.$axios.$get(
-        '/resources/v5/client/disk/info/userCard/single',
-        {
-          params: {
-            userCardId,
-          },
-        },
-      );
-      this.diskInfo = res.data;
-    },
-
-    /**
-     * 轮询连接云机,因为超分需要挂载
-     * @param {number} userCardId
-     * @param {number} 超时
-     */
-    async connect(userCardId) {
-      const getConnectData = () =>
-        this.$axios.$post('/resources/user/cloud/connect', {
-          userCardId,
-        });
-      const res = await getConnectData().catch((error) => {
-        if (error.response.data.status === 5200) {
-          // 云手机挂载中, 开启轮询
-          return new Promise((resolve, reject) => {
-            this.loadingLabel = '云手机挂载中...';
-            const startTime = Date.now();
-            const maxTime = 1000 * 20;
-            this._watchConnectInterval = setInterval(async () => {
-              try {
-                if (Date.now() - startTime >= maxTime) {
-                  throw new Error('云手机挂载超时');
-                }
-                const res = await getConnectData().catch((error) => {
-                  if (error.response.data.status === 5200) {
-                    return false;
-                  }
-                  throw error;
-                });
-
-                if (res) {
-                  clearInterval(this._watchConnectInterval);
-                  resolve(res);
-                }
-              } catch (error) {
-                clearInterval(this._watchConnectInterval);
-                reject(error);
-              }
-            }, 1000);
-          });
-        }
-        throw error;
-      });
-
-      this.connectInfo = res.data;
-      return res;
-
-      // const src = await new Promise((resolve, reject) => {
-      //   const startTime = Date.now();
-      //   this._watchConnectInterval = setInterval(async () => {
-      //     if (Date.now() - startTime >= time) {
-      //       clearInterval(this._watchConnectInterval);
-      //       reject(new Error('挂载超时'));
-      //       return;
-      //     }
-      //     await this.$axios
-      //       .$post('/resources/user/cloud/connect', {
-      //         userCardId,
-      //       })
-      //       .then();
-      //   }, interval);
-      // });
-      // return src;
-    },
-  },
-};
-</script>
-
-<style lang="scss" scoped>
-// .disk {
-//   position: relative;
-//   display: flex;
-//   flex-direction: column;
-//   height: 100%;
-// }
-// .disk-header {
-//   position: relative;
-//   height: 0;
-//   flex: none;
-// }
-// .disk-main {
-//   position: relative;
-//   flex: 1;
-// }
-// .disk-footer {
-//   height: 50px;
-// flex: none;
-// }
-</style>

+ 199 - 0
components/disk/index.vue

@@ -0,0 +1,199 @@
+<template>
+  <div
+    :data-id="diskInfo && diskInfo.id"
+    class="disk reverse flex flex-col h-full"
+  >
+    <header class="disk-header"></header>
+    <main class="disk-main flex-auto">
+      <disk-video :info="connectInfo"></disk-video>
+      <disk-touch :info="connectInfo" @sizeChange="() => {}"></disk-touch>
+    </main>
+    <footer class="disk-footer h-50rpx"></footer>
+    <v-overlay absolute :value="$fetchState.error || $fetchState.pending">
+      <div
+        v-if="$fetchState.pending"
+        class="flex flex-col justify-center items-center"
+      >
+        <v-progress-circular
+          indeterminate
+          class="w-32rpx h-32rpx"
+        ></v-progress-circular>
+        <div v-if="loadingLabel" class="label mt-2">{{ loadingLabel }}</div>
+      </div>
+      <div class="" v-else-if="$fetchState.error">
+        <v-icon>$error</v-icon>
+        <div class="">{{ $fetchState.error.message }}</div>
+        <div class="">
+          <v-btn @click="$router.back()">退出</v-btn>
+        </div>
+      </div>
+    </v-overlay>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Disk',
+  props: {
+    userCardId: {
+      type: Number,
+      default: null,
+    },
+  },
+  data() {
+    return {
+      diskInfo: null,
+      connectInfo: null,
+      // loading: false,
+      loadingLabel: null,
+
+      // 当前生效的分辨率
+      currentWidth: 720,
+      currentHeight: 1280,
+      currentDpi: 1,
+
+      // 当前选中的分辨率
+      activeWidth: 720,
+      activeHeight: 1280,
+      activeDpi: 1,
+
+      // 分辨率列表
+      phoneSizeList: [
+        {
+          id: 1,
+          width: 720,
+          height: 1280,
+          dpi: 1,
+        },
+      ],
+
+      // currentPhoneSizeId: 0,
+      // activePhoneSizeId: 0,
+    };
+  },
+  computed: {
+    // currentPhoneSize() {
+    //   // return this.phoneSizeList.find(v=> v.width=== )
+    // },
+  },
+  async fetch() {
+    // await this.getDiskInfo();
+    if (this.userCardId) {
+      await this.getDiskInfo(this.userCardId);
+      await this.getPhoneSizeList(this.userCardId);
+      await this.connect(this.userCardId);
+    }
+  },
+  watch: {
+    userCardId: '$fetch',
+  },
+  methods: {
+    // 查询云机
+    async getDiskInfo(userCardId) {
+      const res = await this.$axios.$get(
+        '/resources/v5/client/disk/info/userCard/single',
+        {
+          params: {
+            userCardId,
+          },
+        },
+      );
+      this.diskInfo = res.data;
+    },
+    async getPhoneSizeList(userCardId) {
+      try {
+        const res = await this.$axios.$get(
+          '/resources/v5/machine/resolution/getResolvingPower',
+          {
+            params: {
+              userCardId,
+            },
+          },
+        );
+        this.phoneSizeList = res.data;
+      } catch (error) {
+        this.phoneSizeList = [];
+      }
+    },
+
+    /**
+     * 轮询连接云机,因为超分需要挂载
+     * @param {number} userCardId
+     * @param {number} 超时
+     */
+    async connect(userCardId) {
+      // 连接云机
+      const getConnectData = () => {
+        return this.$axios
+          .$post('/resources/user/cloud/connect', {
+            userCardId,
+          })
+          .catch((error) => {
+            if (error.response.data.status === 5200) {
+              this.loadingLabel = '云手机挂载中...';
+            }
+            return error;
+          });
+      };
+
+      const res = await getConnectData();
+
+      if (!res.isAxiosError) {
+        this.connectInfo = res.data;
+        // this.connectInfo.sn = 'RK3931C13019';
+        return res;
+      }
+      // 第一次连接失败,开启轮询
+      const wres = await new Promise((resolve, reject) => {
+        const startTime = Date.now();
+        const maxTime = 1000 * 20;
+        clearInterval(this._watchConnectInterval);
+        this._watchConnectInterval = setInterval(async () => {
+          try {
+            if (Date.now() - startTime >= maxTime) {
+              throw new Error('云手机挂载超时');
+            }
+            const res = await getConnectData();
+            if (!res.isAxiosError) {
+              clearInterval(this._watchConnectInterval);
+              resolve(res);
+            }
+          } catch (error) {
+            clearInterval(this._watchConnectInterval);
+            reject(error);
+          }
+        }, 1000);
+      });
+      this.connectInfo = wres.data;
+      // this.connectInfo.sn = 'RK3923C1201900139';
+      return res;
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+// .disk {
+//   position: relative;
+//   display: flex;
+//   flex-direction: column;
+//   height: 100%;
+// }
+// .disk-header {
+//   position: relative;
+//   height: 0;
+//   flex: none;
+// }
+// .disk-main {
+//   position: relative;
+//   flex: 1;
+// }
+// .disk-footer {
+//   height: 50px;
+// flex: none;
+// }
+.disk-main {
+  position: relative;
+  overflow: hidden;
+}
+</style>

+ 122 - 0
components/disk/touch.vue

@@ -0,0 +1,122 @@
+<template>
+  <div
+    class="disk-touch"
+    @touchstart="ontouchstart"
+    @touchmove="ontouchmove"
+    @touchend="ontouchend"
+  >
+  </div>
+</template>
+
+<script>
+import qs from 'qs';
+export default {
+  name: 'DiskTouch',
+  props: {
+    info: {
+      type: Object,
+      default: null,
+    },
+  },
+  async fetch() {
+    this.info && (await this.initBusinessChannelWebSocket());
+  },
+  watch: {
+    info: '$fetch',
+  },
+  methods: {
+    genSendDataList(event, action) {
+      const width = 720;
+      const height = 1280;
+      return Array.from(event.changedTouches).map((item) => ({
+        data: {
+          action,
+          count: event.touches.length,
+          pointerId: item.identifier,
+          x: (
+            item.clientX *
+            (width / this.$el.getBoundingClientRect().width)
+          ).toFixed(2),
+          y: (
+            item.clientY *
+            (height / this.$el.getBoundingClientRect().height)
+          ).toFixed(2),
+        },
+        type: 'event',
+      }));
+    },
+    initBusinessChannelWebSocket() {
+      this?._wx?.close();
+      const { internetHttps, localIp, sn, cardToken } = this.info;
+      const url = `wss://${internetHttps}/businessChannel${qs.stringify(
+        {
+          cardIp: localIp,
+          token: cardToken,
+          type: 'directives',
+        },
+        { addQueryPrefix: true, encode: false },
+      )}`;
+      const ws = new WebSocket(url);
+      ws.addEventListener('open', (e) => {
+        ws.send(JSON.stringify({ type: 'getVsStatus' }));
+        ws.send(
+          JSON.stringify({ type: 'bitRate', data: { bitRate: 1243000 } }),
+        );
+        ws.send(
+          JSON.stringify({
+            type: 'forwardMsg',
+            data: { code: '3000', desc: '询问是否有在控制' },
+          }),
+        );
+        ws.send(
+          JSON.stringify({
+            type: 'getPhoneSize',
+          }),
+        );
+      });
+      ws.addEventListener('error', (e) => {});
+      ws.addEventListener('message', (event) => {
+        var result =
+          typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
+        console.log(result);
+        if (result.type === 'getPhoneSize') {
+          this.$emit('sizeChange', result.data);
+        }
+      });
+      ws.addEventListener('close', (e) => {});
+      this.$once('hook:beforeDestroy', () => {
+        ws.close();
+      });
+      this._ws = ws;
+    },
+    ontouchstart(event) {
+      this.genSendDataList(event, 0).forEach((item) =>
+        this._ws.send(JSON.stringify(item)),
+      );
+    },
+    ontouchmove(event) {
+      this.genSendDataList(event, 2).forEach((item) =>
+        this._ws.send(JSON.stringify(item)),
+      );
+    },
+    ontouchend(event) {
+      this.genSendDataList(event, 1).forEach((item) =>
+        this._ws.send(JSON.stringify(item)),
+      );
+    },
+  },
+};
+</script>
+
+
+<style lang="scss" scoped>
+.disk-touch {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 1;
+}
+</style>
+

+ 337 - 0
components/disk/video.vue

@@ -0,0 +1,337 @@
+<template>
+  <div class="disk-video">
+    <video
+      v-if="hasMediaSource"
+      :id="videoId"
+      ref="video"
+      class="video"
+      src=""
+      autoplay
+    ></video>
+    <canvas
+      v-else
+      :id="videoId"
+      class="canvas"
+      width="720"
+      height="1280"
+    ></canvas>
+  </div>
+</template>
+
+<script>
+import JMuxer from 'jmuxer';
+import qs from 'qs';
+// bcc校验码计算
+// arry: 要计算的数组
+// 返回计算协议中校验位的校验码
+function calBcc(arry) {
+  let bcc = 0;
+  for (let i = 0; i < arry.length; i++) {
+    bcc ^= arry[i];
+  }
+  return bcc;
+}
+
+function makeFrameExtend(sn, dataType, body) {
+  let index = 0;
+  const dataLen = body.length;
+  const frameLen = dataLen + 26;
+  const outPut = new Uint8Array(frameLen);
+  outPut[index++] = 0x68;
+  outPut[index++] = (dataLen & 0xff000000) >> 24;
+  outPut[index++] = (dataLen & 0x00ff0000) >> 16;
+  outPut[index++] = (dataLen & 0x0000ff00) >> 8;
+  outPut[index++] = dataLen & 0x000000ff;
+  outPut[index++] = 0; // 类型为client
+
+  // sn号赋值,string转ascii
+  for (let i = 0; i < sn.length; i++) {
+    outPut[index++] = sn[i].charCodeAt();
+  }
+
+  outPut[index++] = dataType; // 指定数据类型为json
+  // json string转ascii
+  for (let i = 0; i < body.length; i++) {
+    outPut[index++] = body[i];
+  }
+
+  const bccBuffer = outPut.slice(1, frameLen - 3 + 1); // 忽略协议头和协议尾
+  outPut[index++] = calBcc(bccBuffer);
+  outPut[index++] = 0x16;
+  return outPut;
+}
+
+// 生成鉴权报文
+function VerifyCode(sn, code) {
+  const len = code.length + 1;
+  const codeBuffer = new TextEncoder('utf-8').encode(code); // 获取字符串ascii码
+  const buffer = new Uint8Array(len);
+  buffer[0] = 0x04;
+
+  for (let i = 0; i < codeBuffer.length; i++) {
+    buffer[i + 1] = codeBuffer[i];
+  }
+
+  return makeFrameExtend(sn, 6, buffer);
+}
+
+// I 帧请求报文生成
+function RequestIFrame(sn) {
+  // let sn = "RK3923C1201900139";
+  const outPut = new Uint8Array([0x20]);
+  return makeFrameExtend(sn, 6, outPut);
+}
+
+// 数组打印,调试用
+function PrintArry(data) {
+  let str = '';
+
+  for (let i = 0; i < data.length; i++) {
+    str = str + data[i].toString(16).padStart(2, '0');
+  }
+
+  str = str.toUpperCase();
+  return str;
+}
+// 检查鉴权报文
+function CheckVerifyCode(data) {
+  const dataLen = data.length - 26;
+  const body = data.slice(24, 24 + dataLen);
+  console.log('打印:' + PrintArry(body));
+
+  if (body[3] === 0x03) {
+    return true;
+  }
+
+  return false;
+}
+// 通道配置
+function ConfigChannel(sn) {
+  const outPut = new Uint8Array([0x07]);
+  return makeFrameExtend(sn, 6, outPut);
+}
+
+// 查询屏幕方向
+function GetScreenState(sn) {
+  // let sn = "RK3923C1201900139";
+  const outPut = new Uint8Array([
+    0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x02,
+  ]);
+  return makeFrameExtend(sn, 5, outPut);
+}
+
+// 根据报文识别屏幕方向, 0横屏,1竖屏
+function CheckScreenDirection(data) {
+  if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 1) {
+    if (data[4] === 1 && data[5] === 1) {
+      if (data[6] === 1) {
+        const screen = data[7];
+        return screen;
+      }
+    }
+  }
+}
+
+// 多端登录数据解析
+function checkMultiLoginInfo(input) {
+  const dataLen = input.length - 26; // 得到json 长度
+  const jsonHex = input.slice(24, 24 + dataLen); // 截取json hex二进制数据
+  const jsonStr = new TextDecoder('utf-8').decode(jsonHex);
+  console.log('取得json 字符串:' + jsonStr);
+  const jsonObj = JSON.parse(jsonStr);
+  return jsonObj;
+}
+
+// 切换清晰度
+function makeSharpness(sn, level) {
+  // var sn = "RK3923C1201900139";
+  var jsonObj = {
+    type: 2,
+    data: { definition: level, clientType: 'h5', sceneType: 'cloudPhone' },
+  };
+  var jsonStr = JSON.stringify(jsonObj);
+  var outPut = new TextEncoder('utf-8').encode(jsonStr);
+  return makeFrameExtend(sn, 0xd, outPut);
+}
+export default {
+  name: 'DiskVideo',
+  props: {
+    info: {
+      type: Object,
+      default: null,
+    },
+  },
+  data() {
+    return {
+      hasMediaSource: false,
+      videoId: `video-${Date.now()}`,
+    };
+  },
+  created() {},
+  mounted() {
+    this.initVideo();
+  },
+  async fetch() {
+    this.info && (await this.initPlugflowWebSocket());
+  },
+  watch: {
+    info: '$fetch',
+  },
+  methods: {
+    initPlugflowWebSocket() {
+      this?._ws?.close();
+      const { internetHttps, localIp, sn, cardToken } = this.info;
+      const url = `wss://${internetHttps}/plugflow${qs.stringify(
+        {
+          cardIp: localIp,
+          token: cardToken,
+          type: 'business',
+        },
+        { addQueryPrefix: true },
+      )}`;
+      const ws = new WebSocket(url);
+      ws.binaryType = 'arraybuffer';
+      ws.addEventListener('open', (e) => {
+        ws.send('ping');
+        ws._pingInterval = setInterval(() => {
+          ws.send('ping');
+        }, 1000 * 1);
+        ws.send(VerifyCode(sn, cardToken));
+        // ws.send(makeSharpness(sn, 4));
+        // ws.send(RequestIFrame(sn));
+      });
+      ws.addEventListener('error', (e) => {});
+      ws.addEventListener('message', (event) => {
+        const ParseProto = (data) => {
+          const input = new Uint8Array(data);
+          let duration;
+          let video;
+          let frameType;
+          let audio;
+
+          if (
+            input[0] === 0 &&
+            input[1] === 0 &&
+            input[2] === 0 &&
+            input[3] === 1
+          ) {
+            video = input;
+            duration = 24;
+            const nalType = input[4] & 0x1f;
+            frameType = nalType;
+
+            // if (!isFeed) {
+            //   if (nalType == 0x05 && isVisuable) {
+            //     isFeed = true;
+            //   }
+            // }
+          } else if (input[0] === 0xff) {
+            audio = input;
+            duration = 24;
+          } else if (input[0] === 0x68) {
+            if (input[23] === 0x5c) {
+              console.log('收到消息:' + PrintArry(input));
+
+              if (CheckVerifyCode(input)) {
+                ws.send(ConfigChannel(sn));
+                // ws.send(GetScreenState(sn));
+              } else {
+                // connect('update');
+              }
+            }
+            if (input[23] === 0x05) {
+              // 横竖屏标识
+              const state = CheckScreenDirection(input.slice(24, 24 + 8));
+
+              if (state === 1) {
+                console.log('安卓卡此时竖屏');
+                // 竖屏处理
+                // resolving = 1;
+              } else {
+                console.log('安卓卡此时横屏');
+                // 横屏处理
+                // resolving = 0;
+              }
+            }
+            if (input[23] === 0x0b) {
+              // 多端登录处理, 数据从索引24开始取, input 是接收到的原始数据
+              const jsonobj = checkMultiLoginInfo(input);
+              // console.log(
+              //   '🚀 ~ file: disk.vue ~ line 324 ~ ParseProto ~ jsonobj',
+              //   jsonobj,
+              // );
+            }
+          }
+
+          return {
+            audio,
+            video,
+            duration,
+            frameType,
+          };
+        };
+        const data = ParseProto(event.data); // JAVA服务器转发
+        // console.log(
+        //   '🚀 ~ file: disk.vue ~ line 336 ~ ws.addEventListener ~ data',
+        //   data,
+        // );
+        if (data.video) {
+          this.pushVideo(data);
+        }
+      });
+      ws.addEventListener('close', (event) => {
+        clearInterval(event.currentTarget._pingInterval);
+      });
+      this.$once('hook:beforeDestroy', () => {
+        ws.close();
+      });
+      this._ws = ws;
+    },
+    initVideo() {
+      this.hasMediaSource = !!window.MediaSource;
+      this.hasMediaSource = false;
+      if (this.hasMediaSource) {
+        this._jmuxer = new JMuxer({
+          node: this.videoId,
+          flushingTime: 33,
+          fps: 30,
+          mode: 'video',
+          debug: false,
+        });
+      } else {
+        throw new Error('当前设备不支持MediaSource');
+      }
+    },
+    pushVideo(data) {
+      if (this.hasMediaSource) {
+        this?._jmuxer?.feed({ video: data.video });
+        this.$refs?.video?.play();
+      }
+    },
+  },
+};
+</script>
+
+
+<style lang="scss" scoped>
+.disk-video {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 0;
+}
+.video {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  object-fit: fill;
+}
+.canvas {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+</style>
+