video.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <template>
  2. <div class="disk-video">
  3. <video
  4. v-if="hasMediaSource"
  5. :id="videoId"
  6. ref="video"
  7. class="video"
  8. src=""
  9. autoplay
  10. ></video>
  11. <canvas
  12. v-else
  13. :id="videoId"
  14. class="canvas"
  15. width="720"
  16. height="1280"
  17. ></canvas>
  18. </div>
  19. </template>
  20. <script>
  21. import JMuxer from 'jmuxer';
  22. import qs from 'qs';
  23. // bcc校验码计算
  24. // arry: 要计算的数组
  25. // 返回计算协议中校验位的校验码
  26. function calBcc(arry) {
  27. let bcc = 0;
  28. for (let i = 0; i < arry.length; i++) {
  29. bcc ^= arry[i];
  30. }
  31. return bcc;
  32. }
  33. function makeFrameExtend(sn, dataType, body) {
  34. let index = 0;
  35. const dataLen = body.length;
  36. const frameLen = dataLen + 26;
  37. const outPut = new Uint8Array(frameLen);
  38. outPut[index++] = 0x68;
  39. outPut[index++] = (dataLen & 0xff000000) >> 24;
  40. outPut[index++] = (dataLen & 0x00ff0000) >> 16;
  41. outPut[index++] = (dataLen & 0x0000ff00) >> 8;
  42. outPut[index++] = dataLen & 0x000000ff;
  43. outPut[index++] = 0; // 类型为client
  44. // sn号赋值,string转ascii
  45. for (let i = 0; i < sn.length; i++) {
  46. outPut[index++] = sn[i].charCodeAt();
  47. }
  48. outPut[index++] = dataType; // 指定数据类型为json
  49. // json string转ascii
  50. for (let i = 0; i < body.length; i++) {
  51. outPut[index++] = body[i];
  52. }
  53. const bccBuffer = outPut.slice(1, frameLen - 3 + 1); // 忽略协议头和协议尾
  54. outPut[index++] = calBcc(bccBuffer);
  55. outPut[index++] = 0x16;
  56. return outPut;
  57. }
  58. // 生成鉴权报文
  59. function VerifyCode(sn, code) {
  60. const len = code.length + 1;
  61. const codeBuffer = new TextEncoder('utf-8').encode(code); // 获取字符串ascii码
  62. const buffer = new Uint8Array(len);
  63. buffer[0] = 0x04;
  64. for (let i = 0; i < codeBuffer.length; i++) {
  65. buffer[i + 1] = codeBuffer[i];
  66. }
  67. return makeFrameExtend(sn, 6, buffer);
  68. }
  69. // I 帧请求报文生成
  70. function RequestIFrame(sn) {
  71. // let sn = "RK3923C1201900139";
  72. const outPut = new Uint8Array([0x20]);
  73. return makeFrameExtend(sn, 6, outPut);
  74. }
  75. // 数组打印,调试用
  76. function PrintArry(data) {
  77. let str = '';
  78. for (let i = 0; i < data.length; i++) {
  79. str = str + data[i].toString(16).padStart(2, '0');
  80. }
  81. str = str.toUpperCase();
  82. return str;
  83. }
  84. // 检查鉴权报文
  85. function CheckVerifyCode(data) {
  86. const dataLen = data.length - 26;
  87. const body = data.slice(24, 24 + dataLen);
  88. console.log('打印:' + PrintArry(body));
  89. if (body[3] === 0x03) {
  90. return true;
  91. }
  92. return false;
  93. }
  94. // 通道配置
  95. function ConfigChannel(sn) {
  96. const outPut = new Uint8Array([0x07]);
  97. return makeFrameExtend(sn, 6, outPut);
  98. }
  99. // 查询屏幕方向
  100. function GetScreenState(sn) {
  101. // let sn = "RK3923C1201900139";
  102. const outPut = new Uint8Array([
  103. 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x02,
  104. ]);
  105. return makeFrameExtend(sn, 5, outPut);
  106. }
  107. // 根据报文识别屏幕方向, 0横屏,1竖屏
  108. function CheckScreenDirection(data) {
  109. if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 1) {
  110. if (data[4] === 1 && data[5] === 1) {
  111. if (data[6] === 1) {
  112. const screen = data[7];
  113. return screen;
  114. }
  115. }
  116. }
  117. }
  118. // 多端登录数据解析
  119. function checkMultiLoginInfo(input) {
  120. const dataLen = input.length - 26; // 得到json 长度
  121. const jsonHex = input.slice(24, 24 + dataLen); // 截取json hex二进制数据
  122. const jsonStr = new TextDecoder('utf-8').decode(jsonHex);
  123. console.log('取得json 字符串:' + jsonStr);
  124. const jsonObj = JSON.parse(jsonStr);
  125. return jsonObj;
  126. }
  127. // 切换清晰度
  128. function makeSharpness(sn, level) {
  129. // var sn = "RK3923C1201900139";
  130. var jsonObj = {
  131. type: 2,
  132. data: { definition: level, clientType: 'h5', sceneType: 'cloudPhone' },
  133. };
  134. var jsonStr = JSON.stringify(jsonObj);
  135. var outPut = new TextEncoder('utf-8').encode(jsonStr);
  136. return makeFrameExtend(sn, 0xd, outPut);
  137. }
  138. export default {
  139. name: 'DiskVideo',
  140. props: {
  141. info: {
  142. type: Object,
  143. default: null,
  144. },
  145. },
  146. data() {
  147. return {
  148. hasMediaSource: false,
  149. videoId: `video-${Date.now()}`,
  150. };
  151. },
  152. created() {},
  153. mounted() {
  154. this.initVideo();
  155. },
  156. async fetch() {
  157. this.info && (await this.initPlugflowWebSocket());
  158. },
  159. watch: {
  160. info: '$fetch',
  161. },
  162. methods: {
  163. initPlugflowWebSocket() {
  164. this?._ws?.close();
  165. const { internetHttps, localIp, sn, cardToken } = this.info;
  166. const url = `wss://${internetHttps}/plugflow${qs.stringify(
  167. {
  168. cardIp: localIp,
  169. token: cardToken,
  170. type: 'business',
  171. },
  172. { addQueryPrefix: true },
  173. )}`;
  174. const ws = new WebSocket(url);
  175. ws.binaryType = 'arraybuffer';
  176. ws.addEventListener('open', (e) => {
  177. ws.send('ping');
  178. ws._pingInterval = setInterval(() => {
  179. ws.send('ping');
  180. }, 1000 * 1);
  181. ws.send(VerifyCode(sn, cardToken));
  182. // ws.send(makeSharpness(sn, 4));
  183. // ws.send(RequestIFrame(sn));
  184. });
  185. ws.addEventListener('error', (e) => {});
  186. ws.addEventListener('message', (event) => {
  187. const ParseProto = (data) => {
  188. const input = new Uint8Array(data);
  189. let duration;
  190. let video;
  191. let frameType;
  192. let audio;
  193. if (
  194. input[0] === 0 &&
  195. input[1] === 0 &&
  196. input[2] === 0 &&
  197. input[3] === 1
  198. ) {
  199. video = input;
  200. duration = 24;
  201. const nalType = input[4] & 0x1f;
  202. frameType = nalType;
  203. // if (!isFeed) {
  204. // if (nalType == 0x05 && isVisuable) {
  205. // isFeed = true;
  206. // }
  207. // }
  208. } else if (input[0] === 0xff) {
  209. audio = input;
  210. duration = 24;
  211. } else if (input[0] === 0x68) {
  212. if (input[23] === 0x5c) {
  213. console.log('收到消息:' + PrintArry(input));
  214. if (CheckVerifyCode(input)) {
  215. ws.send(ConfigChannel(sn));
  216. // ws.send(GetScreenState(sn));
  217. } else {
  218. // connect('update');
  219. }
  220. }
  221. if (input[23] === 0x05) {
  222. // 横竖屏标识
  223. const state = CheckScreenDirection(input.slice(24, 24 + 8));
  224. if (state === 1) {
  225. console.log('安卓卡此时竖屏');
  226. // 竖屏处理
  227. // resolving = 1;
  228. } else {
  229. console.log('安卓卡此时横屏');
  230. // 横屏处理
  231. // resolving = 0;
  232. }
  233. }
  234. if (input[23] === 0x0b) {
  235. // 多端登录处理, 数据从索引24开始取, input 是接收到的原始数据
  236. const jsonobj = checkMultiLoginInfo(input);
  237. // console.log(
  238. // '🚀 ~ file: disk.vue ~ line 324 ~ ParseProto ~ jsonobj',
  239. // jsonobj,
  240. // );
  241. }
  242. }
  243. return {
  244. audio,
  245. video,
  246. duration,
  247. frameType,
  248. };
  249. };
  250. const data = ParseProto(event.data); // JAVA服务器转发
  251. // console.log(
  252. // '🚀 ~ file: disk.vue ~ line 336 ~ ws.addEventListener ~ data',
  253. // data,
  254. // );
  255. if (data.video) {
  256. this.pushVideo(data);
  257. }
  258. });
  259. ws.addEventListener('close', (event) => {
  260. clearInterval(event.currentTarget._pingInterval);
  261. });
  262. this.$once('hook:beforeDestroy', () => {
  263. ws.close();
  264. });
  265. this._ws = ws;
  266. },
  267. initVideo() {
  268. this.hasMediaSource = !!window.MediaSource;
  269. this.hasMediaSource = false;
  270. if (this.hasMediaSource) {
  271. this._jmuxer = new JMuxer({
  272. node: this.videoId,
  273. flushingTime: 33,
  274. fps: 30,
  275. mode: 'video',
  276. debug: false,
  277. });
  278. } else {
  279. throw new Error('当前设备不支持MediaSource');
  280. }
  281. },
  282. pushVideo(data) {
  283. if (this.hasMediaSource) {
  284. this?._jmuxer?.feed({ video: data.video });
  285. this.$refs?.video?.play();
  286. }
  287. },
  288. },
  289. };
  290. </script>
  291. <style lang="scss" scoped>
  292. .disk-video {
  293. position: absolute;
  294. top: 0;
  295. left: 0;
  296. right: 0;
  297. bottom: 0;
  298. z-index: 0;
  299. }
  300. .video {
  301. position: relative;
  302. width: 100%;
  303. height: 100%;
  304. object-fit: fill;
  305. }
  306. .canvas {
  307. position: relative;
  308. width: 100%;
  309. height: 100%;
  310. }
  311. </style>