splitview.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. <template>
  2. <div style="height: 100%">
  3. <el-card id="devicePosition" shadow="never" style="height: calc(100% - 40px);border: 0;" :body-style="{ padding: '0px' }" class="border-none">
  4. <el-container
  5. v-loading="loading"
  6. style="height: 100%"
  7. :element-loading-text="t('sip.splitview998531-0')"
  8. >
  9. <el-aside width="250px" style="background-color: #ffffff">
  10. <DeviceTree :click-event="clickEvent" />
  11. </el-aside>
  12. <el-main style="padding: 0">
  13. <div
  14. height="5vh"
  15. style="text-align: left; font-size: 17px; line-height: 5vh; margin-bottom: 10px"
  16. >
  17. {{ t('sip.splitview998531-1') }}
  18. <el-button
  19. type="success"
  20. style="margin-left: 10px"
  21. :class="{ active: spilt == 1 }"
  22. @click="spilt = 1"
  23. :icon="FullScreen"
  24. plain
  25. size="small"
  26. >
  27. {{ t('sip.splitview998531-2') }}
  28. </el-button>
  29. <el-button
  30. type="info"
  31. style="margin-left: 10px"
  32. :class="{ active: spilt == 4 }"
  33. @click="spilt = 4"
  34. :icon="Menu"
  35. plain
  36. size="small"
  37. >
  38. {{ t('sip.splitview998531-3') }}
  39. </el-button>
  40. <el-button
  41. type="warning"
  42. style="margin-left: 10px"
  43. :class="{ active: spilt == 9 }"
  44. @click="spilt = 9"
  45. :icon="Grid"
  46. plain
  47. size="small"
  48. >
  49. {{ t('sip.splitview998531-4') }}
  50. </el-button>
  51. </div>
  52. <div style="height: 85vh; display: flex; flex-wrap: wrap">
  53. <div
  54. v-for="i in spilt"
  55. :key="i"
  56. class="play-box"
  57. :style="liveStyle"
  58. :class="{ redborder: playerIdx == i - 1 }"
  59. @click="playerIdx = i - 1"
  60. >
  61. <div
  62. v-if="!videoUrl[i - 1]"
  63. style="color: #ffffff; font-size: 30px; font-weight: bold"
  64. >{{ i }}</div
  65. >
  66. <player
  67. v-else
  68. :ref="(el) => setPlayerRef(el, i - 1)"
  69. :video-url="videoUrl[i - 1]"
  70. fluent
  71. autoplay
  72. @screenshot="shot"
  73. class="player-wrap"
  74. @destroy="destroy"
  75. />
  76. </div>
  77. </div>
  78. </el-main>
  79. </el-container>
  80. </el-card>
  81. </div>
  82. </template>
  83. <script setup>
  84. import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
  85. import { useRoute } from 'vue-router'
  86. import { FullScreen, Menu, Grid } from '@element-plus/icons-vue'
  87. import player from './components/player/jessibuca.vue'
  88. import DeviceTree from './components/player/DeviceTree.vue'
  89. import { startPlay } from '@/api/pms/video/channel'
  90. import { listSipDeviceChannel } from '@/api/pms/video/sipdevice';
  91. const { t } = useI18n()
  92. // 使用组合式API
  93. const route = useRoute()
  94. // refs
  95. const playerRefs = ref([])
  96. // 数据状态
  97. const videoUrl = ref([''])
  98. const spilt = ref(1) //分屏
  99. const playerIdx = ref(0) //激活播放器
  100. const updateLooper = ref(0) //数据刷新轮训标志
  101. const count = ref(15)
  102. const total = ref(0)
  103. const loading = ref(false)
  104. // 设置播放器引用
  105. const setPlayerRef = (el, index) => {
  106. if (el) {
  107. playerRefs.value[index] = el
  108. }
  109. }
  110. // 计算属性 - 直播样式
  111. const liveStyle = computed(() => {
  112. let style = { width: '81%', height: '99%' }
  113. switch (spilt.value) {
  114. case 4:
  115. style = { width: '40%', height: '49%' }
  116. break
  117. case 9:
  118. style = { width: '27%', height: '32%' }
  119. break
  120. }
  121. nextTick(() => {
  122. for (let i = 0; i < spilt.value; i++) {
  123. const player = playerRefs.value[i]
  124. if (player && typeof player.updatePlayerDomSize === 'function') {
  125. player.updatePlayerDomSize()
  126. }
  127. }
  128. })
  129. return style
  130. })
  131. // 监听分屏变化
  132. watch(
  133. () => spilt.value,
  134. async (newSplit, oldSplit) => {
  135. // 只有当分屏数增加(例如从1到4,或从4到9)且有当前选中设备时才执行
  136. if (newSplit > oldSplit && currentDevice.value) {
  137. console.log(`分屏从 ${oldSplit} 变为 ${newSplit},自动填充设备 ${currentDevice.value.name} 的通道`)
  138. // 延迟执行,确保DOM更新完成
  139. await nextTick()
  140. loading.value = true
  141. try {
  142. // 1. 重新获取当前设备的通道
  143. const channels = await getDeviceChannels(currentDevice.value.id)
  144. if (!channels || channels.length === 0) {
  145. console.warn('该设备下没有可用通道')
  146. return
  147. }
  148. // 2. 重新分配通道到所有播放器
  149. await assignChannelsToPlayers(channels)
  150. } catch (error) {
  151. console.error('分屏变化时填充通道出错:', error)
  152. ElMessage.error('自动填充通道失败')
  153. } finally {
  154. loading.value = false
  155. }
  156. }
  157. // 可选:当分屏数减少时,可以清空多余的播放器
  158. else if (newSplit < oldSplit) {
  159. // 清空超出当前分屏数的播放器
  160. for (let i = newSplit; i < oldSplit; i++) {
  161. const newVideoUrls = [...videoUrl.value]
  162. newVideoUrls[i] = ''
  163. videoUrl.value = newVideoUrls
  164. clear(i + 1) // 清理存储的数据
  165. }
  166. }
  167. },
  168. { immediate: false } // 不需要立即执行
  169. )
  170. // 监听路由变化
  171. watch(
  172. () => route.fullPath,
  173. () => {
  174. checkPlayByParam()
  175. }
  176. )
  177. // 生命周期钩子
  178. onMounted(() => {
  179. checkPlayByParam()
  180. })
  181. onUnmounted(() => {
  182. clearTimeout(updateLooper.value)
  183. })
  184. // 销毁事件
  185. const destroy = (idx) => {
  186. console.log(idx)
  187. clear(idx.substring(idx.length - 1))
  188. }
  189. const currentDevice = ref(null) // 存储当前选中的设备信息
  190. // 点击事件
  191. const clickEvent = async (data) => {
  192. // 情况1:点击的是设备节点 (type === 0)
  193. if (data.type === 0) {
  194. currentDevice.value = {
  195. id: data.userData?.serialNumber,
  196. name: data.name,
  197. data: data
  198. }
  199. const deviceId = data.userData?.serialNumber; // 从树节点数据中提取设备ID
  200. if (!deviceId) return;
  201. loading.value = true;
  202. try {
  203. // 1. 获取该设备下的所有通道
  204. const channels = await getDeviceChannels(deviceId);
  205. if (!channels || channels.length === 0) {
  206. console.warn('该设备下没有可用通道');
  207. return;
  208. }
  209. // 2. 将通道智能分配给播放器
  210. await assignChannelsToPlayers(channels);
  211. } catch (error) {
  212. console.error('处理设备通道时出错:', error);
  213. } finally {
  214. loading.value = false;
  215. }
  216. }
  217. // 情况2:点击的是具体的通道节点,保持原有逻辑不变
  218. else if (data.userData?.channelSipId) {
  219. sendDevicePush(data.userData);
  220. }
  221. };
  222. const getDeviceChannels = async (deviceId) => {
  223. try {
  224. const response = await listSipDeviceChannel(deviceId);
  225. console.log('获取设备通道列表成功:', response);
  226. // 假设接口返回的是通道数组,且每个通道对象结构符合 { basicData: { channelSipId, ... }, ... }
  227. return response || [];
  228. } catch (error) {
  229. console.error('获取设备通道列表失败:', error);
  230. return [];
  231. }
  232. };
  233. const assignChannelsToPlayers = async (channels) => {
  234. const currentSplit = spilt.value
  235. console.log(`分配 ${channels.length} 个通道到 ${currentSplit} 个播放窗口`)
  236. // 决定从哪个播放器窗口开始分配:始终从第一个窗口(0)开始
  237. // 这样保证每次分屏变化时都能从头开始重新分配
  238. const startPlayerIndex = 0
  239. // 决定从哪个通道开始取:总是从第一个通道开始
  240. const startChannelIndex = 0
  241. // 循环,为当前分屏布局中的每个位置分配通道
  242. for (let screenPosition = 0; screenPosition < currentSplit; screenPosition++) {
  243. const channelIndex = startChannelIndex + screenPosition
  244. const targetPlayerIndex = startPlayerIndex + screenPosition
  245. // 延迟执行,避免同时发起大量请求
  246. await new Promise(resolve => setTimeout(resolve, 50))
  247. if (channelIndex < channels.length) {
  248. // 有对应通道,开始播放
  249. const channelData = channels[channelIndex]
  250. // 构建符合 sendDevicePush 要求的参数格式
  251. const playData = {
  252. deviceSipId: channelData.deviceId,
  253. channelSipId: channelData.basicData?.channelSipId || channelData.id,
  254. name: channelData.name || `通道${channelIndex + 1}`,
  255. ...channelData.basicData
  256. }
  257. console.log(`将通道 ${playData.name} 分配给播放器 ${targetPlayerIndex}`)
  258. await sendDevicePush(playData, targetPlayerIndex)
  259. } else {
  260. // 没有更多通道,清空当前位置
  261. const newVideoUrls = [...videoUrl.value]
  262. newVideoUrls[targetPlayerIndex] = ''
  263. videoUrl.value = newVideoUrls
  264. // 清理存储的数据
  265. clear(targetPlayerIndex + 1)
  266. console.log(`播放器 ${targetPlayerIndex} 没有可用通道,已清空`)
  267. }
  268. }
  269. }
  270. // 通知设备上传媒体流
  271. const sendDevicePush = (itemData, targetIndex = null) => {
  272. // 关键修改:允许从外部传入 targetIndex,默认使用当前激活的 playerIdx
  273. const playIndex = targetIndex !== null ? targetIndex : playerIdx.value;
  274. save(itemData, playIndex); // 保存数据也要使用正确的索引
  275. let deviceId = itemData.deviceSipId;
  276. let channelId = itemData.channelSipId;
  277. loading.value = true;
  278. return startPlay(deviceId, channelId)
  279. .then((response) => {
  280. let res = response;
  281. if (window.location.protocol === 'http:') {
  282. itemData.playUrl = res.ws_flv;
  283. } else {
  284. itemData.playUrl = res.wss_flv;
  285. }
  286. itemData.streamId = res.streamId;
  287. setPlayUrl(itemData.playUrl, playIndex); // 播放URL设置到指定位置
  288. })
  289. .finally(() => {
  290. loading.value = false;
  291. });
  292. };
  293. // 设置播放URL
  294. const setPlayUrl = (url, idx) => {
  295. const newVideoUrls = [...videoUrl.value]
  296. newVideoUrls[idx] = url
  297. videoUrl.value = newVideoUrls
  298. setTimeout(() => {
  299. window.localStorage.setItem('videoUrl', JSON.stringify(videoUrl.value))
  300. }, 100)
  301. }
  302. // 检查播放参数
  303. const checkPlayByParam = () => {
  304. let { deviceId, channelId } = route.query
  305. if (deviceId && channelId) {
  306. sendDevicePush({ deviceId, channelId })
  307. }
  308. }
  309. // 截图处理
  310. const shot = (e) => {
  311. var base64ToBlob = function (code) {
  312. let parts = code.split(';base64,')
  313. let contentType = parts[0].split(':')[1]
  314. let raw = window.atob(parts[1])
  315. let rawLength = raw.length
  316. let uInt8Array = new Uint8Array(rawLength)
  317. for (let i = 0; i < rawLength; ++i) {
  318. uInt8Array[i] = raw.charCodeAt(i)
  319. }
  320. return new Blob([uInt8Array], {
  321. type: contentType
  322. })
  323. }
  324. let aLink = document.createElement('a')
  325. let blob = base64ToBlob(e)
  326. let evt = document.createEvent('HTMLEvents')
  327. evt.initEvent('click', true, true)
  328. aLink.download = 't("sip.splitview998531-5")'
  329. aLink.href = URL.createObjectURL(blob)
  330. aLink.click()
  331. }
  332. // 保存数据
  333. const save = (item, index) => {
  334. let dataStr = window.localStorage.getItem('playData') || '[]';
  335. let data = JSON.parse(dataStr);
  336. data[index] = item;
  337. window.localStorage.setItem('playData', JSON.stringify(data));
  338. };
  339. // 清除数据
  340. const clear = (idx) => {
  341. let dataStr = window.localStorage.getItem('playData') || '[]'
  342. let data = JSON.parse(dataStr)
  343. data[idx - 1] = null
  344. console.log(data)
  345. window.localStorage.setItem('playData', JSON.stringify(data))
  346. }
  347. </script>
  348. <style scoped>
  349. .btn {
  350. margin: 0 10px;
  351. }
  352. .btn:hover {
  353. color: #409eff;
  354. }
  355. .btn.active {
  356. color: #409eff;
  357. }
  358. .redborder {
  359. border: 2px solid red !important;
  360. }
  361. .play-box {
  362. background-color: #000000;
  363. border: 1px solid #505050;
  364. display: flex;
  365. align-items: center;
  366. justify-content: center;
  367. margin-right: 10px;
  368. position: relative;
  369. border-radius: 5px;
  370. }
  371. .player-wrap {
  372. position: absolute;
  373. top: 0px;
  374. height: 100% !important;
  375. }
  376. </style>