|
@@ -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>
|
|
|
|
+
|