video.vue 8.6 KB

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