index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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 } 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 }[]>([])
  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
  114. if (node.itemValue.indexOf('&') != -1) {
  115. types = node.itemValue.substring(node.itemValue.indexOf('&') + 1)
  116. } else {
  117. types = ''
  118. }
  119. let res = await getDevice({ ids: types })
  120. if (res.msgTxt.length != 0) {
  121. res.msgTxt[0].datalist.forEach(el => {
  122. el.pid = node.id
  123. el.isFolder = false
  124. el.title = el.strinstallpos
  125. el.id = el.deviceID
  126. })
  127. listArr[0].children.forEach(v => {
  128. if (v.id == node.id) {
  129. v.children = res.msgTxt[0].datalist
  130. }
  131. })
  132. }
  133. searchParam.devKind = node.itemValue
  134. searchParam.strType = ''
  135. await getVideoAddrs()
  136. } else {
  137. getVideoAddrsSon(node.deviceID)
  138. }
  139. } else {
  140. searchParam.devKind = ''
  141. searchParam.strType = ''
  142. await getVideoAddrs()
  143. }
  144. getVideo()
  145. };
  146. //点击详情跳转
  147. function onDetail(node) {
  148. let type = listArr[0].children.filter(v => v.id == node.pid)[0].itemValue
  149. console.log(type, 'type--------')
  150. switch (type) {
  151. case 'pulping'://注浆
  152. router.push('/grout-home')
  153. break;
  154. case 'window'://自动风窗
  155. router.push('/monitorChannel/monitor-window?id=' + node.deviceID)
  156. break;
  157. case 'gate'://自动风门
  158. router.push('/monitorChannel/monitor-gate?id=' + node.deviceID)
  159. break;
  160. case 'fanlocal'://局部风机
  161. router.push('/monitorChannel/monitor-fanlocal?id=' + node.deviceID + '&deviceType=fanlocal')
  162. break;
  163. case 'fanmain'://主风机
  164. router.push('/monitorChannel/monitor-fanmain?id=' + node.deviceID)
  165. break;
  166. case 'forcFan'://压风机
  167. router.push('/forcFan/home')
  168. break;
  169. case 'pump'://瓦斯抽采泵
  170. router.push('/monitorChannel/gasPump-home')
  171. break;
  172. case 'nitrogen'://制氮
  173. router.push('/nitrogen-home')
  174. break;
  175. }
  176. }
  177. async function getVideoAddrs() {
  178. clearCamera();
  179. playerList.value = []
  180. let res = await list({ ...searchParam, pageSize: pageSize.value, pageNo: current.value })
  181. total.value = res['total'] || 0
  182. if (res.records.length != 0) {
  183. const cameraList = <{ name: string, addr: string }[]>[]
  184. const cameras = res.records
  185. for (let i = 0; i < cameras.length; i++) {
  186. const item = cameras[i];
  187. if (item['devicekind'] === 'toHKRtsp') {
  188. // 从海康平台接口获取视频流
  189. try {
  190. const data = await cameraAddr({ cameraCode: item['addr'] });
  191. if (data) {
  192. cameraList.push({ name: item['name'], addr: data['url'] });
  193. }
  194. // cameraList.push({
  195. // name: item['name'],
  196. // // addr: 'http://219.151.31.38/liveplay-kk.rtxapp.com/live/program/live/hnwshd/4000000/mnf.m3u8'
  197. // addr: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8',
  198. // });
  199. } catch (error) {
  200. }
  201. } else {
  202. if (item['addr'].includes('0.0.0.0')) {
  203. item['addr'] = item['addr'].replace('0.0.0.0', window.location.hostname)
  204. }
  205. cameraList.push({ name: item['name'], addr: item['addr'] });
  206. }
  207. }
  208. addrList.value = cameraList
  209. }
  210. }
  211. async function getVideoAddrsSon(Id) {
  212. clearCamera();
  213. playerList.value = []
  214. let res = await getVentanalyCamera({ deviceid: Id })
  215. if (res.records.length != 0) {
  216. const cameraList = <{ name: string, addr: string }[]>[]
  217. const cameras = res.records
  218. for (let i = 0; i < cameras.length; i++) {
  219. const item = cameras[i];
  220. if (item['devicekind'] === 'toHKRtsp') {
  221. // 从海康平台接口获取视频流
  222. try {
  223. const data = await cameraAddr({ cameraCode: item['addr'] });
  224. if (data && data['url']) {
  225. cameraList.push({ name: item['name'], addr: data['url'] });
  226. }
  227. // cameraList.push({
  228. // name: item['name'],
  229. // // addr: 'http://219.151.31.38/liveplay-kk.rtxapp.com/live/program/live/hnwshd/4000000/mnf.m3u8'
  230. // addr: 'https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.mp4/.m3u8',
  231. // });
  232. } catch (error) {
  233. }
  234. } else {
  235. if (item['addr'].includes('0.0.0.0')) {
  236. item['addr'] = item['addr'].replace('0.0.0.0', window.location.hostname)
  237. }
  238. cameraList.push({ name: item['name'], addr: item['addr'] });
  239. }
  240. }
  241. addrList.value = cameraList
  242. }
  243. }
  244. function onChange(page) {
  245. current.value = page;
  246. getVideoAddrs().then(() => {
  247. getVideo()
  248. })
  249. }
  250. function getVideo() {
  251. const ip = VUE_APP_URL.webRtcUrl;
  252. for (let i = 0; i < addrList.value.length; i++) {
  253. const item = addrList.value[i]
  254. if (item.addr.startsWith('rtsp://')) {
  255. const dom = document.getElementById('video' + i) as HTMLVideoElement
  256. dom.muted = true;
  257. dom.volume = 0
  258. const webRtcServer = new window['WebRtcStreamer'](dom, location.protocol + ip)
  259. webRtcServerList.push(webRtcServer)
  260. webRtcServer.connect(item.addr)
  261. } else {
  262. setNoRtspVideo('player' + i, item.addr)
  263. }
  264. }
  265. }
  266. function setNoRtspVideo(id, videoAddr) {
  267. const fileExtension = videoAddr.split('.').pop();
  268. if (fileExtension === 'flv') {
  269. const player = new Player({
  270. lang: 'zh',
  271. id: id,
  272. url: videoAddr,
  273. width: 589,
  274. height: 330,
  275. poster: '/src/assets/images/vent/noSinge.png',
  276. plugins: [FlvPlugin],
  277. fluid: true,
  278. autoplay: true,
  279. isLive: true,
  280. playsinline: true,
  281. screenShot: true,
  282. whitelist: [''],
  283. ignores: ['time'],
  284. closeVideoClick: true,
  285. customConfig: {
  286. isClickPlayBack: false
  287. },
  288. flv: {
  289. retryCount: 3, // 重试 3 次,默认值
  290. retryDelay: 1000, // 每次重试间隔 1 秒,默认值
  291. loadTimeout: 10000, // 请求超时时间为 10 秒,默认值
  292. fetchOptions: {
  293. // 该参数会透传给 fetch,默认值为 undefined
  294. mode: 'cors'
  295. },
  296. targetLatency: 10, // 直播目标延迟,默认 10 秒
  297. maxLatency: 20, // 直播允许的最大延迟,默认 20 秒
  298. disconnectTime: 10, // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
  299. maxJumpDistance: 10,
  300. }
  301. });
  302. playerList.value.push(player)
  303. }
  304. if (fileExtension === 'm3u8') {
  305. let player
  306. if (document.createElement('video').canPlayType('application/vnd.apple.mpegurl')) {
  307. // 原生支持 hls 播放
  308. player = new Player({
  309. lang: 'zh',
  310. id: id,
  311. url: videoAddr,
  312. width: 589,
  313. height: 330,
  314. isLive: true,
  315. autoplay: true,
  316. autoplayMuted: true,
  317. cors: true,
  318. poster: '/src/assets/images/vent/noSinge.png',
  319. hls: {
  320. retryCount: 3, // 重试 3 次,默认值
  321. retryDelay: 1000, // 每次重试间隔 1 秒,默认值
  322. loadTimeout: 10000, // 请求超时时间为 10 秒,默认值
  323. fetchOptions: {
  324. // 该参数会透传给 fetch,默认值为 undefined
  325. mode: 'cors'
  326. },
  327. targetLatency: 10, // 直播目标延迟,默认 10 秒
  328. maxLatency: 20, // 直播允许的最大延迟,默认 20 秒
  329. disconnectTime: 10, // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
  330. maxJumpDistance: 10,
  331. }
  332. })
  333. } else if (HlsPlugin.isSupported()) { // 第一步
  334. player = new Player({
  335. lang: 'zh',
  336. id: id,
  337. url: videoAddr,
  338. width: 589,
  339. height: 330,
  340. isLive: true,
  341. autoplay: true,
  342. autoplayMuted: true,
  343. plugins: [HlsPlugin], // 第二步
  344. poster: '/src/assets/images/vent/noSinge.png',
  345. hls: {
  346. retryCount: 3, // 重试 3 次,默认值
  347. retryDelay: 1000, // 每次重试间隔 1 秒,默认值
  348. loadTimeout: 10000, // 请求超时时间为 10 秒,默认值
  349. fetchOptions: {
  350. // 该参数会透传给 fetch,默认值为 undefined
  351. mode: 'cors'
  352. },
  353. targetLatency: 10, // 直播目标延迟,默认 10 秒
  354. maxLatency: 20, // 直播允许的最大延迟,默认 20 秒
  355. disconnectTime: 10, // 直播断流时间,默认 0 秒,(独立使用时等于 maxLatency)
  356. maxJumpDistance: 10,
  357. }
  358. })
  359. }
  360. playerList.value.push(player)
  361. }
  362. }
  363. function goFullScreen(domId) {
  364. const videoDom = document.getElementById(domId) as HTMLVideoElement
  365. if (videoDom.requestFullscreen) {
  366. videoDom.requestFullscreen()
  367. videoDom.play()
  368. } else if (videoDom.mozRequestFullscreen) {
  369. videoDom.mozRequestFullscreen()
  370. videoDom.play()
  371. } else if (videoDom.webkitRequestFullscreen) {
  372. videoDom.webkitRequestFullscreen()
  373. videoDom.play()
  374. } else if (videoDom.msRequestFullscreen) {
  375. videoDom.msRequestFullscreen()
  376. videoDom.play()
  377. }
  378. }
  379. function clearCamera() {
  380. const num = webRtcServerList.length
  381. for (let i = 0; i < num; i++) {
  382. webRtcServerList[i].disconnect()
  383. webRtcServerList[i] = null
  384. }
  385. for (let i = 0; i < playerList.value.length; i++) {
  386. const player = playerList.value[i]
  387. if (player.destroy) player.destroy()
  388. }
  389. playerList.value = []
  390. }
  391. onMounted(async () => {
  392. await getVideoAddrs()
  393. // getTreeList()
  394. getCameraDevKindList()
  395. getVideo()
  396. })
  397. onUnmounted(() => {
  398. clearCamera()
  399. })
  400. </script>
  401. <style lang="less">
  402. .camera-container {
  403. position: relative;
  404. width: calc(100% - 30px);
  405. height: calc(100% - 84px);
  406. display: flex;
  407. margin: 15px;
  408. justify-content: space-between;
  409. align-items: center;
  410. .left-area {
  411. width: 15%;
  412. height: 100%;
  413. padding: 20px;
  414. border: 1px solid #99e8ff66;
  415. background: #27546e1a;
  416. box-shadow: 0px 0px 20px 7px rgba(145, 233, 254, 0.7) inset;
  417. -moz-box-shadow: 0px 0px 20px 7px rgba(145, 233, 254, 0.7) inset;
  418. -webkit-box-shadow: 0px 0px 50px 1px rgb(149 235 255 / 5%) inset;
  419. box-sizing: border-box;
  420. // lxh
  421. .iconfont {
  422. color: #fff;
  423. font-size: 12px;
  424. margin-left: 5px;
  425. }
  426. }
  427. .right-area {
  428. width: 85%;
  429. height: 100%;
  430. padding: 0px 0px 0px 15px;
  431. box-sizing: border-box;
  432. .camera-box {
  433. width: 100%;
  434. height: calc(100% - 60px);
  435. display: flex;
  436. justify-content: space-around;
  437. align-items: flex-start;
  438. flex-wrap: wrap;
  439. overflow-y: auto;
  440. }
  441. .camera-box1 {
  442. width: 100%;
  443. height: calc(100% - 60px);
  444. display: flex;
  445. justify-content: flex-start;
  446. align-items: flex-start;
  447. flex-wrap: wrap;
  448. overflow-y: auto;
  449. }
  450. .player-box {
  451. width: 626px;
  452. height: 370px;
  453. padding: 17px 18px;
  454. background: url('/@/assets/images/vent/camera_bg.png');
  455. background-size: 100% 100%;
  456. position: relative;
  457. margin: 10px;
  458. .player-name {
  459. font-size: 14px;
  460. position: absolute;
  461. top: 15px;
  462. right: 15px;
  463. color: #fff;
  464. background-color: hsla(0, 0%, 50%, .5);
  465. border-radius: 2px;
  466. padding: 1px 5px;
  467. max-width: 120px;
  468. overflow: hidden;
  469. white-space: nowrap;
  470. text-overflow: ellipsis;
  471. z-index: 999;
  472. }
  473. .click-box {
  474. position: absolute;
  475. width: 100%;
  476. height: 100%;
  477. top: 0;
  478. left: 0;
  479. }
  480. }
  481. .pagination {
  482. width: 100%;
  483. height: 60px;
  484. display: flex;
  485. justify-content: center;
  486. align-items: center;
  487. }
  488. }
  489. }
  490. :deep(video) {
  491. width: 100% !important;
  492. height: 100% !important;
  493. object-fit: cover !important;
  494. }
  495. </style>