index copy.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. <template>
  2. <div class="camera-container">
  3. <div class="left-area">
  4. <cameraTree :selected="selected" :list="listArr" :draggable="true" @detail-node="onDetail" @on-click="onClick">
  5. <template #icon="{ item }">
  6. <template v-if="item.isFolder">
  7. <SvgIcon v-if="item.expanded" size="18" name="file-open" />
  8. <SvgIcon v-else size="18" name="file-close" />
  9. </template>
  10. <treeIcon class="iconfont" :title="item.title" v-else />
  11. </template>
  12. <template #operation="{ type }">
  13. <!-- <i class="iconfont icon-eyeoutlined"></i> -->
  14. <span style="color: #ccc; font-size: 12px">详情</span>
  15. </template>
  16. </cameraTree>
  17. </div>
  18. <div class="right-area" v-if="addrList.length != 0">
  19. <div class="vent-flex-row-wrap" :class="addrList.length == 1 ? 'camera-box1' : 'camera-box'">
  20. <div v-for="(item, index) in addrList" :key="index" class="player-box">
  21. <div class="player-name">{{ item.name }}</div>
  22. <div style="padding-top: 3px">
  23. <template v-if="item.addr.startsWith('rtsp://')">
  24. <video :id="`video${index}`" muted autoplay></video>
  25. <div class="click-box" @dblclick="goFullScreen(`video${index}`)"></div>
  26. </template>
  27. <template v-else>
  28. <div :id="'player' + index"></div>
  29. </template>
  30. </div>
  31. </div>
  32. </div>
  33. <div class="pagination">
  34. <Pagination v-model:current="current" v-model:page-size="pageSize" :total="total" @change="onChange" />
  35. </div>
  36. </div>
  37. <div class="camera-box" v-else>
  38. <Empty />
  39. </div>
  40. </div>
  41. </template>
  42. <script lang="ts" setup>
  43. import { onMounted, onUnmounted, ref, reactive, computed } from 'vue';
  44. import { useRouter } from 'vue-router';
  45. import { Pagination, Empty } from 'ant-design-vue';
  46. import { list, cameraAddr, getCameraDevKind, getDevice, getVentanalyCamera } from './camera.api';
  47. import Player, { I18N } from 'xgplayer';
  48. import ZH from 'xgplayer/es/lang/zh-cn';
  49. import HlsPlugin from 'xgplayer-hls';
  50. import FlvPlugin from 'xgplayer-flv';
  51. import 'xgplayer/dist/index.min.css';
  52. import cameraTree from './common/cameraTree.vue';
  53. import { SvgIcon } from '/@/components/Icon';
  54. import treeIcon from './common/Icon/treeIcon.vue';
  55. //当前选中树节点
  56. let selected = reactive<any>({
  57. id: null,
  58. pid: null,
  59. title: '',
  60. isFolder: false,
  61. });
  62. //tree菜单列表
  63. let listArr = reactive<any[]>([]);
  64. let searchParam = reactive({
  65. devKind: '',
  66. strType: '',
  67. });
  68. I18N.use(ZH);
  69. let router = useRouter(); //路由
  70. const pageSize = ref(4);
  71. const current = ref(1);
  72. const total = ref(0);
  73. const playerList = ref([]);
  74. const webRtcServerList = <any[]>[];
  75. let addrList = ref<{ name: string; addr: string; cameraRate: number; devicekind: string }[]>([]);
  76. async function getCameraDevKindList() {
  77. let res = await getCameraDevKind();
  78. if (res.length != 0) {
  79. listArr.length = 0;
  80. listArr.push({
  81. pid: 'root',
  82. isFolder: true,
  83. expanded: true,
  84. title: '全部',
  85. id: 0,
  86. children: [],
  87. });
  88. res.forEach((el) => {
  89. el.pid = 0;
  90. el.isFolder = true;
  91. el.expanded = false;
  92. el.title = el.itemText;
  93. el.id = el.subDictId;
  94. el.children = [];
  95. listArr[0].children.push(el);
  96. });
  97. selected.id = listArr[0].id;
  98. selected.pid = listArr[0].pid;
  99. selected.title = listArr[0].title;
  100. selected.isFolder = listArr[0].isFolder;
  101. }
  102. }
  103. //点击目录
  104. async function onClick(node) {
  105. if (selected.title === node.title && selected.id === node.id) return;
  106. current.value = 1;
  107. selected.id = node.id;
  108. selected.pid = node.pid;
  109. selected.title = node.title;
  110. selected.isFolder = node.isFolder;
  111. if (node.pid != 'root') {
  112. if (node.isFolder) {
  113. let types, devicetype;
  114. if (node.itemValue.indexOf('&') != -1) {
  115. types = node.itemValue.substring(node.itemValue.indexOf('&') + 1);
  116. devicetype = node.itemValue.substring(0, node.itemValue.indexOf('&'));
  117. } else {
  118. types = '';
  119. devicetype = '';
  120. }
  121. let res = await getDevice({ ids: types, devicetype: devicetype });
  122. if (res.msgTxt.length != 0) {
  123. res.msgTxt[0].datalist.forEach((el) => {
  124. el.pid = node.id;
  125. el.isFolder = false;
  126. el.title = el.strinstallpos;
  127. el.id = el.deviceID;
  128. });
  129. listArr[0].children.forEach((v) => {
  130. if (v.id == node.id) {
  131. v.children = res.msgTxt[0].datalist;
  132. }
  133. });
  134. }
  135. searchParam.devKind = node.itemValue;
  136. searchParam.strType = '';
  137. await getVideoAddrs();
  138. getVideo();
  139. } else {
  140. await getVideoAddrsSon(node.deviceID);
  141. getVideo();
  142. }
  143. } else {
  144. searchParam.devKind = '';
  145. searchParam.strType = '';
  146. await getVideoAddrs();
  147. getVideo();
  148. }
  149. }
  150. //点击详情跳转
  151. function onDetail(node) {
  152. let str = listArr[0].children.filter((v) => v.id == node.pid)[0].itemValue;
  153. let type = str.indexOf('&') != -1 ? str.substring(0, str.indexOf('&')) : '';
  154. console.log(type, 'type--------');
  155. switch (type) {
  156. case 'pulping': //注浆
  157. router.push('/grout-home');
  158. break;
  159. case 'window': //自动风窗
  160. router.push('/monitorChannel/monitor-window?id=' + node.deviceID);
  161. break;
  162. case 'gate': //自动风门
  163. router.push('/monitorChannel/monitor-gate?id=' + node.deviceID + '&deviceType=' + node.deviceType);
  164. break;
  165. case 'fanlocal': //局部风机
  166. router.push('/monitorChannel/monitor-fanlocal?id=' + node.deviceID + '&deviceType=fanlocal');
  167. break;
  168. case 'fanmain': //主风机
  169. router.push('/monitorChannel/monitor-fanmain?id=' + node.deviceID);
  170. break;
  171. case 'forcFan': //压风机
  172. router.push('/forcFan/home');
  173. break;
  174. case 'pump': //瓦斯抽采泵
  175. router.push('/monitorChannel/gasPump-home');
  176. break;
  177. case 'nitrogen': //制氮
  178. router.push('/nitrogen-home');
  179. break;
  180. }
  181. }
  182. async function getVideoAddrs() {
  183. clearCamera();
  184. playerList.value = [];
  185. let paramKind = searchParam.devKind.substring(0, searchParam.devKind.indexOf('&'));
  186. let res = await list({ devKind: paramKind, strType: searchParam.strType, pageSize: pageSize.value, pageNo: current.value });
  187. total.value = res['total'] || 0;
  188. if (res.records.length != 0) {
  189. const cameraList = <{ name: string; addr: string; cameraRate: number; devicekind: string }[]>[];
  190. const cameras = res.records;
  191. for (let i = 0; i < cameras.length; i++) {
  192. const item = cameras[i];
  193. if (
  194. item['devicekind'] === 'toHKRtsp' ||
  195. item['devicekind'] === 'toHKHLs' ||
  196. item['devicekind'] === 'HLL' ||
  197. item['devicekind'] === 'YZG_URL'
  198. ) {
  199. // 从海康平台接口获取视频流
  200. const videoType = item['devicekind'] === 'toHKRtsp' ? 'rtsp' : '';
  201. const devicekindType = item['devicekind'] === 'YZG_URL' ? 'YZG_URL' : '';
  202. try {
  203. const data = await cameraAddr({ devicekind: devicekindType, cameraCode: item['addr'], videoType });
  204. if (data && data['url']) {
  205. cameraList.push({ name: item['name'], addr: data['url'], cameraRate: item['cameraRate'], devicekind: item['devicekind'] });
  206. }
  207. // cameraList.push({
  208. // name: item['name'],
  209. // // addr: 'http://219.151.31.38/liveplay-kk.rtxapp.com/live/program/live/hnwshd/4000000/mnf.m3u8'
  210. // addr: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8',
  211. // });
  212. } catch (error) {}
  213. } else {
  214. if (item['addr'].includes('0.0.0.0')) {
  215. item['addr'] = item['addr'].replace('0.0.0.0', window.location.hostname);
  216. }
  217. cameraList.push({ name: item['name'], addr: item['addr'], cameraRate: item['cameraRate'], devicekind: item['devicekind'] });
  218. }
  219. }
  220. addrList.value = cameraList;
  221. console.log(addrList.value, ' addrList.value-------------');
  222. }
  223. }
  224. async function getVideoAddrsSon(Id) {
  225. clearCamera();
  226. playerList.value = [];
  227. let res = await getVentanalyCamera({ deviceid: Id });
  228. if (res.records.length != 0) {
  229. const cameraList = <{ name: string; addr: string; cameraRate: number; devicekind: string }[]>[];
  230. const cameras = res.records;
  231. for (let i = 0; i < cameras.length; i++) {
  232. const item = cameras[i];
  233. if (
  234. item['devicekind'] === 'toHKRtsp' ||
  235. item['devicekind'] === 'toHKHLs' ||
  236. item['devicekind'] === 'HLL' ||
  237. item['devicekind'] === 'YZG_URL'
  238. ) {
  239. // 从海康平台接口获取视频流
  240. const videoType = item['devicekind'] === 'toHKRtsp' ? 'rtsp' : '';
  241. const devicekindType = item['devicekind'] === 'YZG_URL' ? 'YZG_URL' : '';
  242. try {
  243. const data = await cameraAddr({ devicekind: devicekindType, cameraCode: item['addr'], videoType });
  244. if (data && data['url']) {
  245. cameraList.push({ name: item['name'], addr: data['url'], cameraRate: item['cameraRate'], devicekind: item['devicekind'] });
  246. }
  247. // cameraList.push({
  248. // name: item['name'],
  249. // // addr: 'http://219.151.31.38/liveplay-kk.rtxapp.com/live/program/live/hnwshd/4000000/mnf.m3u8'
  250. // addr: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8',
  251. // });
  252. } catch (error) {}
  253. } else {
  254. if (item['addr'].includes('0.0.0.0')) {
  255. item['addr'] = item['addr'].replace('0.0.0.0', window.location.hostname);
  256. }
  257. cameraList.push({ name: item['name'], addr: item['addr'], cameraRate: item['cameraRate'], devicekind: item['devicekind'] });
  258. }
  259. }
  260. addrList.value = cameraList;
  261. }
  262. }
  263. async function onChange(page) {
  264. current.value = page;
  265. await getVideoAddrs();
  266. getVideo();
  267. }
  268. function getVideo() {
  269. const ip = VUE_APP_URL.webRtcUrl;
  270. for (let i = 0; i < addrList.value.length; i++) {
  271. const item = addrList.value[i];
  272. if (item.addr.startsWith('rtsp://')) {
  273. const dom = document.getElementById('video' + i) as HTMLVideoElement;
  274. dom.muted = true;
  275. dom.volume = 0;
  276. const webRtcServer = new window['WebRtcStreamer'](dom, location.protocol + ip);
  277. webRtcServerList.push(webRtcServer);
  278. webRtcServer.connect(item.addr);
  279. } else {
  280. setNoRtspVideo('player' + i, item.addr, item.cameraRate, item.devicekind);
  281. }
  282. }
  283. }
  284. function setNoRtspVideo(id, videoAddr, cameraRate, devicekind) {
  285. const fileExtension = videoAddr.split('.').pop();
  286. if (fileExtension === 'flv' || devicekind == 'flv') {
  287. const player = new Player({
  288. lang: 'zh',
  289. id: id,
  290. url: videoAddr,
  291. width: 589,
  292. height: 330,
  293. poster: '/src/assets/images/vent/noSinge.png',
  294. plugins: [FlvPlugin],
  295. fluid: true,
  296. autoplay: true,
  297. isLive: true,
  298. playsinline: true,
  299. screenShot: true,
  300. whitelist: [''],
  301. ignores: ['time', 'progress', 'play', 'i18n', 'volume', 'fullscreen', 'screenShot', 'playbackRate'],
  302. closeVideoClick: true,
  303. customConfig: {
  304. isClickPlayBack: false,
  305. },
  306. defaultPlaybackRate: cameraRate || 1,
  307. controls: false,
  308. flv: {
  309. retryCount: 3, // 重试 3 次,默认值
  310. retryDelay: 1000, // 每次重试间隔 1 秒,默认值
  311. loadTimeout: 10000, // 请求超时时间为 10 秒,默认值
  312. fetchOptions: {
  313. // 该参数会透传给 fetch,默认值为 undefined
  314. mode: 'cors',
  315. },
  316. targetLatency: 10, // 直播目标延迟,默认 10 秒
  317. maxLatency: 20, // 直播允许的最大延迟,默认 20 秒
  318. disconnectTime: 10, // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
  319. maxJumpDistance: 10,
  320. },
  321. });
  322. playerList.value.push(player);
  323. }
  324. if (fileExtension === 'm3u8' || devicekind == 'm3u8') {
  325. let player;
  326. if (document.createElement('video').canPlayType('application/vnd.apple.mpegurl')) {
  327. // 原生支持 hls 播放
  328. player = new Player({
  329. lang: 'zh',
  330. id: id,
  331. url: videoAddr,
  332. width: 589,
  333. height: 330,
  334. isLive: true,
  335. autoplay: true,
  336. autoplayMuted: true,
  337. cors: true,
  338. ignores: ['time', 'progress', 'play', 'i18n', 'volume', 'fullscreen', 'screenShot', 'playbackRate'],
  339. poster: '/src/assets/images/vent/noSinge.png',
  340. defaultPlaybackRate: cameraRate || 1,
  341. controls: false,
  342. hls: {
  343. retryCount: 3, // 重试 3 次,默认值
  344. retryDelay: 1000, // 每次重试间隔 1 秒,默认值
  345. loadTimeout: 10000, // 请求超时时间为 10 秒,默认值
  346. fetchOptions: {
  347. // 该参数会透传给 fetch,默认值为 undefined
  348. mode: 'cors',
  349. },
  350. targetLatency: 10, // 直播目标延迟,默认 10 秒
  351. maxLatency: 20, // 直播允许的最大延迟,默认 20 秒
  352. disconnectTime: 10, // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
  353. maxJumpDistance: 10,
  354. },
  355. });
  356. } else if (HlsPlugin.isSupported()) {
  357. // 第一步
  358. player = new Player({
  359. lang: 'zh',
  360. id: id,
  361. url: videoAddr,
  362. width: 589,
  363. height: 330,
  364. isLive: true,
  365. autoplay: true,
  366. autoplayMuted: true,
  367. plugins: [HlsPlugin], // 第二步
  368. poster: '/src/assets/images/vent/noSinge.png',
  369. ignores: ['time', 'progress', 'play', 'i18n', 'volume', 'fullscreen', 'screenShot', 'playbackRate'],
  370. defaultPlaybackRate: cameraRate || 1,
  371. controls: false,
  372. hls: {
  373. retryCount: 3, // 重试 3 次,默认值
  374. retryDelay: 1000, // 每次重试间隔 1 秒,默认值
  375. loadTimeout: 10000, // 请求超时时间为 10 秒,默认值
  376. fetchOptions: {
  377. // 该参数会透传给 fetch,默认值为 undefined
  378. mode: 'cors',
  379. },
  380. targetLatency: 10, // 直播目标延迟,默认 10 秒
  381. maxLatency: 20, // 直播允许的最大延迟,默认 20 秒
  382. disconnectTime: 10, // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
  383. maxJumpDistance: 10,
  384. },
  385. });
  386. }
  387. playerList.value.push(player);
  388. }
  389. }
  390. function goFullScreen(domId) {
  391. const videoDom = document.getElementById(domId) as HTMLVideoElement;
  392. if (videoDom.requestFullscreen) {
  393. videoDom.requestFullscreen();
  394. videoDom.play();
  395. } else if (videoDom.mozRequestFullscreen) {
  396. videoDom.mozRequestFullscreen();
  397. videoDom.play();
  398. } else if (videoDom.webkitRequestFullscreen) {
  399. videoDom.webkitRequestFullscreen();
  400. videoDom.play();
  401. } else if (videoDom.msRequestFullscreen) {
  402. videoDom.msRequestFullscreen();
  403. videoDom.play();
  404. }
  405. }
  406. function clearCamera() {
  407. const num = webRtcServerList.length;
  408. for (let i = 0; i < num; i++) {
  409. if (webRtcServerList[i]) {
  410. webRtcServerList[i].disconnect();
  411. webRtcServerList[i] = null;
  412. }
  413. }
  414. for (let i = 0; i < playerList.value.length; i++) {
  415. const player = playerList.value[i];
  416. if (player.destroy) player.destroy();
  417. }
  418. playerList.value = [];
  419. }
  420. onMounted(async () => {
  421. await getCameraDevKindList();
  422. await getVideoAddrs();
  423. getVideo();
  424. });
  425. onUnmounted(() => {
  426. clearCamera();
  427. });
  428. </script>
  429. <style lang="less">
  430. @import '/@/design/theme.less';
  431. @{theme-deepblue} {
  432. .camera-container {
  433. --image-camera_bg: url('/@/assets/images/themify/deepblue/vent/camera_bg.png');
  434. }
  435. }
  436. .camera-container {
  437. --image-camera_bg: url('/@/assets/images/vent/camera_bg.png');
  438. position: relative;
  439. width: calc(100% - 30px);
  440. height: calc(100% - 84px);
  441. display: flex;
  442. margin: 15px;
  443. justify-content: space-between;
  444. align-items: center;
  445. .left-area {
  446. width: 15%;
  447. height: 100%;
  448. padding: 20px;
  449. border: 1px solid #99e8ff66;
  450. background: #27546e1a;
  451. box-shadow: 0px 0px 20px 7px rgba(145, 233, 254, 0.7) inset;
  452. -moz-box-shadow: 0px 0px 20px 7px rgba(145, 233, 254, 0.7) inset;
  453. -webkit-box-shadow: 0px 0px 50px 1px rgb(149 235 255 / 5%) inset;
  454. box-sizing: border-box;
  455. // lxh
  456. .iconfont {
  457. color: #fff;
  458. font-size: 12px;
  459. margin-left: 5px;
  460. }
  461. }
  462. .right-area {
  463. width: 85%;
  464. height: 100%;
  465. padding: 0px 0px 0px 15px;
  466. box-sizing: border-box;
  467. .camera-box {
  468. width: 100%;
  469. height: calc(100% - 60px);
  470. display: flex;
  471. justify-content: space-around;
  472. align-items: flex-start;
  473. flex-wrap: wrap;
  474. overflow-y: auto;
  475. }
  476. .camera-box1 {
  477. width: 100%;
  478. height: calc(100% - 60px);
  479. display: flex;
  480. justify-content: flex-start;
  481. align-items: flex-start;
  482. flex-wrap: wrap;
  483. overflow-y: auto;
  484. }
  485. .player-box {
  486. width: 626px;
  487. height: 370px;
  488. padding: 17px 18px;
  489. background: var(--image-camera_bg);
  490. background-size: 100% 100%;
  491. position: relative;
  492. margin: 10px;
  493. .player-name {
  494. font-size: 14px;
  495. position: absolute;
  496. top: 35px;
  497. right: 15px;
  498. color: #fff;
  499. background-color: hsla(0, 0%, 50%, 0.5);
  500. border-radius: 2px;
  501. padding: 1px 5px;
  502. max-width: 120px;
  503. overflow: hidden;
  504. white-space: nowrap;
  505. text-overflow: ellipsis;
  506. z-index: 999;
  507. }
  508. .click-box {
  509. position: absolute;
  510. width: 100%;
  511. height: 100%;
  512. top: 0;
  513. left: 0;
  514. }
  515. }
  516. .pagination {
  517. width: 100%;
  518. height: 60px;
  519. display: flex;
  520. justify-content: center;
  521. align-items: center;
  522. }
  523. }
  524. }
  525. :deep(video) {
  526. width: 100% !important;
  527. height: 100% !important;
  528. object-fit: cover !important;
  529. }
  530. </style>