Commit 35fda30655885af62fd526a0ec2cebbaa4827ac2

Authored by xp.Huang
2 parents 77820111 232fe757

Merge branch 'fix/multiple-camera-loading-play' into 'main_dev'

fix: 修复多个摄像头播放问题

See merge request yunteng/thingskit-view!211
1 <template> 1 <template>
2 - <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }">  
3 - <video  
4 - crossOrigin="anonymous"  
5 - :id="`my-player${index}`"  
6 - ref="videoRef"  
7 - class="video-js my-video vjs-theme-city vjs-big-play-centered"  
8 - ></video>  
9 - </div> 2 + <n-spin size="medium" :show="showLoading"
  3 + :style="{background: '#000',width: baseSize?.w + 'px', height: baseSize?.h + 'px' }">
  4 + <template #description> 视频正在努力加载中...... </template>
  5 + <div>
  6 + <div ref="playerContainerElRef" class="go-content-box"
  7 + :style="{ width: baseSize?.w + 'px', height: baseSize?.h + 'px', background: '#000' }">
  8 +
  9 + </div>
  10 + </div>
  11 + </n-spin>
10 </template> 12 </template>
11 <script setup lang="ts" name="VideoPlay"> 13 <script setup lang="ts" name="VideoPlay">
12 -import { onMounted, ref, onUnmounted, watch, unref, nextTick } from 'vue' 14 +import { onMounted, ref, onUnmounted, watch, unref, PropType, shallowRef } from 'vue'
13 import videojs from 'video.js' 15 import videojs from 'video.js'
14 import 'videojs-flvjs-es6' 16 import 'videojs-flvjs-es6'
15 import type { VideoJsPlayerOptions } from 'video.js' 17 import type { VideoJsPlayerOptions } from 'video.js'
16 import 'video.js/dist/video-js.min.css' 18 import 'video.js/dist/video-js.min.css'
17 import { getJwtToken, getShareJwtToken } from '@/utils/external/auth' 19 import { getJwtToken, getShareJwtToken } from '@/utils/external/auth'
18 import { isShareMode } from '@/views/share/hook' 20 import { isShareMode } from '@/views/share/hook'
19 -import { getOpenFlvPlayUrl, closeFlvPlay } from '@/api/external/flvPlay' 21 +import { getOpenFlvPlayUrl, closeFlvPlay, getVideoControlStart } from '@/api/external/flvPlay'
20 import { useFingerprint } from '@/utils/external/useFingerprint' 22 import { useFingerprint } from '@/utils/external/useFingerprint'
21 import { GetResult } from '@fingerprintjs/fingerprintjs' 23 import { GetResult } from '@fingerprintjs/fingerprintjs'
22 -import { VideoPlayerType } from '../config' 24 +import { AccessModeEnum, VideoPlayerType } from '../config'
  25 +import { getVideoUrl } from '@/api/external/common'
23 26
24 const props = defineProps({ 27 const props = defineProps({
25 sourceSrc: { 28 sourceSrc: {
26 type: String 29 type: String
27 }, 30 },
  31 + option: {
  32 + type: Object
  33 + },
  34 +
  35 + baseSize: {
  36 + type: Object as PropType<{ w: number; h: number }>
  37 + },
28 autoPlay: { 38 autoPlay: {
29 type: Boolean 39 type: Boolean
30 }, 40 },
@@ -47,13 +57,27 @@ const props = defineProps({ @@ -47,13 +57,27 @@ const props = defineProps({
47 } 57 }
48 }) 58 })
49 59
  60 +
  61 +const { getResult } = useFingerprint()
  62 +
  63 +const showLoading = ref<boolean>(false)
  64 +
  65 +const sourceSrc = ref<string | null>(props.sourceSrc || '')
  66 +
  67 +
  68 +// video实例对象
  69 +const videoPlayer = shallowRef<videojs.Player | null>(null)
  70 +const playerContainerElRef = shallowRef<HTMLElement | null>(null)
  71 +
  72 +const fingerprintResult = ref<Nullable<GetResult>>(null)
  73 +
50 const isRtspProtocol = (url: string) => { 74 const isRtspProtocol = (url: string) => {
51 const reg = /^rtsp:\/\//g 75 const reg = /^rtsp:\/\//g
52 return reg.test(url) 76 return reg.test(url)
53 } 77 }
54 78
55 const getVideoTypeByUrl = (url = '') => { 79 const getVideoTypeByUrl = (url = '') => {
56 - if(!url) return; 80 + if (!url) return;
57 try { 81 try {
58 const { protocol, pathname } = new URL(url) 82 const { protocol, pathname } = new URL(url)
59 if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv 83 if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv
@@ -68,13 +92,6 @@ const getVideoTypeByUrl = (url = '') => { @@ -68,13 +92,6 @@ const getVideoTypeByUrl = (url = '') => {
68 } 92 }
69 } 93 }
70 94
71 -// video标签  
72 -const videoRef = ref<HTMLElement | null>(null)  
73 -  
74 -// video实例对象  
75 -let videoPlayer: videojs.Player | null = null  
76 -  
77 -const fingerprintResult = ref<Nullable<GetResult>>(null)  
78 95
79 //options配置 96 //options配置
80 const options: VideoJsPlayerOptions & Recordable = { 97 const options: VideoJsPlayerOptions & Recordable = {
@@ -103,88 +120,130 @@ const options: VideoJsPlayerOptions & Recordable = { @@ -103,88 +120,130 @@ const options: VideoJsPlayerOptions & Recordable = {
103 } 120 }
104 } 121 }
105 122
106 -const { getResult } = useFingerprint()  
107 123
108 async function getSource() { 124 async function getSource() {
109 fingerprintResult.value = await getResult() 125 fingerprintResult.value = await getResult()
110 - let src = props.sourceSrc || ''  
111 - if (isRtspProtocol(props.sourceSrc!)) { 126 + let src = unref(sourceSrc) || ''
  127 + if (isRtspProtocol(unref(sourceSrc)!)) {
112 src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '') 128 src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '')
113 } 129 }
114 return [ 130 return [
115 { 131 {
116 - type: getVideoTypeByUrl(props.sourceSrc), 132 + type: getVideoTypeByUrl(src),
117 src 133 src
118 } 134 }
119 ] 135 ]
120 } 136 }
121 137
  138 +//针对萤石云或者海康威视,根据视频id获取播放流地址
  139 +const getVideoUrlById = async (id: string) => {
  140 + const res = await getVideoUrl(id)
  141 + if (!res) return
  142 + const { url } = res.data
  143 + return url
  144 +}
  145 +
  146 +//针对gbt28181,根据设备id和通道号获取播放流地址
  147 +const getVideoControlList = async (deviceId: string, channelId: string) => {
  148 + const {
  149 + data: { flv }
  150 + } = await getVideoControlStart({
  151 + deviceId,
  152 + channelId
  153 + })
  154 + return flv
  155 +}
  156 +
  157 +//针对自定义地址,直接获取地址
  158 +const getCustomUrl = (url: string) => {
  159 + return url
  160 +}
  161 +
  162 +
  163 +function createVideoElement() {
  164 + if (!unref(playerContainerElRef)) return
  165 + unref(playerContainerElRef)!.innerHTML = ''
  166 + const video = document.createElement('video')
  167 + video.setAttribute('crossOrigin', 'anonymous')
  168 + video.style.setProperty('width', '100%')
  169 + video.style.setProperty('height', '100%')
  170 + video.classList.add('video-js', 'my-video', 'vjs-theme-city', 'vjs-big-play-centered')
  171 + unref(playerContainerElRef)?.appendChild(video)
  172 + return video
  173 +}
  174 +
  175 +
122 // 初始化videojs 176 // 初始化videojs
123 -const initVideo = async () => {  
124 - await nextTick()  
125 - try {  
126 - if (videoRef.value) {  
127 - // 创建 video 实例  
128 - options.sources = await getSource()  
129 - if (options.sources && options.sources.length) {  
130 - if (isRtspProtocol(props.sourceSrc || '')) {  
131 - options.flvjs = {  
132 - ...(options.flvjs || {}),  
133 - config: {  
134 - headers: {  
135 - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`  
136 - }  
137 - }  
138 - }  
139 - }  
140 - if (options.sources.at(-1)?.src){  
141 - videoPlayer = videojs(videoRef.value, options)  
142 - //fix 修复videojs解决直播延时的问题  
143 - videoPlayer?.on('timeupdate', function () {  
144 - // 计算表最新推流的时间和现在播放器播放推流的时间  
145 - let differTime =  
146 - (videoPlayer as any as videojs.Player)?.buffered()?.end(0) -  
147 - (videoPlayer as any as videojs.Player)?.currentTime() // 差值小于1.5s时根据1倍速进行播放  
148 - if (differTime < 1.5) {  
149 - videoPlayer?.playbackRate(1)  
150 - } // 差值大于1.5s小于10s根据1.2倍速进行播放  
151 - if (differTime < 10 && differTime > 1.5) {  
152 - videoPlayer?.playbackRate(1.2)  
153 - } // 差值大于10s时进行重新加载直播流  
154 - if (differTime > 10) {  
155 - initVideo()  
156 - }  
157 - }) // 177 +const createVideoPlayer = async () => {
  178 + if (unref(videoPlayer)) dispose()
  179 +
  180 + options.sources = await getSource()
  181 +
  182 + if (isRtspProtocol(unref(sourceSrc) || '')) {
  183 + options.flvjs = {
  184 + ...(options.flvjs || {}),
  185 + config: {
  186 + headers: {
  187 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`
158 } 188 }
159 } 189 }
160 } 190 }
161 - } catch (e) {  
162 - console.log(e)  
163 } 191 }
  192 + const video = createVideoElement()
  193 + if (!video) return
  194 +
  195 + videoPlayer.value = videojs(video!, options) //fix 修复videojs解决直播延时的问题
  196 +
  197 + videoPlayer.value?.on('timeupdate', function () {
  198 + // 计算表最新推流的时间和现在播放器播放推流的时间
  199 + let differTime =
  200 + (unref(videoPlayer))!.buffered()?.end(0) -
  201 + (unref(videoPlayer))!.currentTime() // 差值小于1.5s时根据1倍速进行播放
  202 + if (differTime < 1.5) {
  203 + videoPlayer.value?.playbackRate(1)
  204 + } // 差值大于1.5s小于10s根据1.2倍速进行播放
  205 + if (differTime < 10 && differTime > 1.5) {
  206 + videoPlayer.value?.playbackRate(1.2)
  207 + } // 差值大于10s时进行重新加载直播流
  208 + if (differTime > 10) {
  209 + createVideoPlayer()
  210 + }
  211 + })
164 } 212 }
165 213
166 -watch(  
167 - () => props.sourceSrc,  
168 - async () => {  
169 - (props as any ).sourceSrc = ''  
170 - if(!props.sourceSrc) return;  
171 - await nextTick()  
172 - if(unref(fingerprintResult)!.visitorId!) {  
173 - closeFlvPlay(props.sourceSrc!, unref(fingerprintResult)!.visitorId!) 214 +const getVideosUrl = async () => {
  215 + try {
  216 + showLoading.value = true
  217 + videoPlayer.value?.src('');
  218 + const { option } = props || {}
  219 + const { accessMode, id, channelId, deviceId, customUrl } = option || {}
  220 + if (accessMode === AccessModeEnum.Streaming) {
  221 + return await getVideoUrlById(id)
  222 + } else if (accessMode === AccessModeEnum.GBT28181) {
  223 + return await getVideoControlList(deviceId, channelId)
  224 + } else {
  225 + return await getCustomUrl(customUrl)
174 } 226 }
175 - videoPlayer?.src('');  
176 - initVideo()  
177 - videoPlayer?.src({  
178 - type: getVideoTypeByUrl(props.sourceSrc),  
179 - src: props.sourceSrc!  
180 - })  
181 - videoPlayer?.load()  
182 - videoPlayer?.play()  
183 - },  
184 - {  
185 - immediate: true 227 + } finally {
  228 + showLoading.value = false
186 } 229 }
187 -) 230 +}
  231 +
  232 +
  233 +watch(() => props.option, async () => {
  234 + console.log(props, 'prop')
  235 + dispose()
  236 + sourceSrc.value = await getVideosUrl()
  237 + createVideoPlayer()
  238 +})
  239 +
  240 +
  241 +
  242 +// 销毁
  243 +function dispose() {
  244 + unref(videoPlayer)?.dispose()
  245 + videoPlayer.value = null
  246 +}
188 247
189 watch( 248 watch(
190 () => props.autoPlay, 249 () => props.autoPlay,
@@ -192,13 +251,15 @@ watch( @@ -192,13 +251,15 @@ watch(
192 if (newData) { 251 if (newData) {
193 handleVideoPlay() 252 handleVideoPlay()
194 } else { 253 } else {
195 - videoPlayer?.pause() 254 + videoPlayer.value?.pause()
196 } 255 }
197 } 256 }
198 ) 257 )
199 258
200 -onMounted(() => {  
201 - initVideo() 259 +onMounted(async () => {
  260 + dispose()
  261 + sourceSrc.value = await getVideosUrl()
  262 + createVideoPlayer()
202 }) 263 })
203 264
204 onUnmounted(() => { 265 onUnmounted(() => {
@@ -209,14 +270,15 @@ onUnmounted(() => { @@ -209,14 +270,15 @@ onUnmounted(() => {
209 }) 270 })
210 271
211 //播放 272 //播放
212 -const handleVideoPlay = () => videoPlayer?.play() 273 +const handleVideoPlay = () => videoPlayer.value?.play()
213 274
214 //暂停和销毁 275 //暂停和销毁
215 -const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause() 276 +const handleVideoDispose = () => videoPlayer.value?.dispose() && videoPlayer.value?.pause()
216 277
217 </script> 278 </script>
218 279
219 <style lang="scss" scoped> 280 <style lang="scss" scoped>
  281 +
220 .go-content-box { 282 .go-content-box {
221 display: flex; 283 display: flex;
222 align-items: center; 284 align-items: center;
@@ -229,3 +291,8 @@ const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause() @@ -229,3 +291,8 @@ const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
229 } 291 }
230 } 292 }
231 </style> 293 </style>
  294 +<style>
  295 +.vjs-poster {
  296 + background-size: 100% !important;
  297 +}
  298 +</style>
@@ -64,7 +64,8 @@ export const option = { @@ -64,7 +64,8 @@ export const option = {
64 url: null, 64 url: null,
65 sourceType: sourceTypeEnum.CUSTOM, 65 sourceType: sourceTypeEnum.CUSTOM,
66 organization: '', 66 organization: '',
67 - autoPlay: true 67 + autoPlay: true,
  68 + accessMode:'', channelId:'', deviceId:'', customUrl:''
68 } 69 }
69 ], 70 ],
70 } 71 }
@@ -176,13 +176,10 @@ const handleChecked = ({ target }: Recordable, _: object, index: number) => { @@ -176,13 +176,10 @@ const handleChecked = ({ target }: Recordable, _: object, index: number) => {
176 176
177 const handleSelect = (_: string, e: videoList, index: number) => { 177 const handleSelect = (_: string, e: videoList, index: number) => {
178 const { accessMode, id, channelId, deviceId, customUrl } = e 178 const { accessMode, id, channelId, deviceId, customUrl } = e
179 - if (accessMode === AccessModeEnum.Streaming) {  
180 - getVideoUrlById(e, id, index)  
181 - } else if (accessMode === AccessModeEnum.GBT28181) {  
182 - getVideoControlList(deviceId, channelId, index)  
183 - } else {  
184 - getCustomUrl(customUrl, index)  
185 - } 179 + props.optionData.dataset[index] = {
  180 + ...props.optionData.dataset[index],
  181 + accessMode, id, channelId, deviceId, customUrl,url:''
  182 + } as any
186 } 183 }
187 184
188 onMounted(() => { 185 onMounted(() => {
@@ -4,12 +4,12 @@ @@ -4,12 +4,12 @@
4 <div v-for="(item, index) in option.dataset" :key="index" :class="item.className" :style="item.sty"> 4 <div v-for="(item, index) in option.dataset" :key="index" :class="item.className" :style="item.sty">
5 <VideoPlay 5 <VideoPlay
6 :autoPlay="item.autoPlay" 6 :autoPlay="item.autoPlay"
  7 + :option="item"
7 :name="item.name" 8 :name="item.name"
8 :avatar="item.avatar" 9 :avatar="item.avatar"
9 :key="item + index" 10 :key="item + index"
10 :sourceSrc="item.url" 11 :sourceSrc="item.url"
11 - :w="w"  
12 - :h="h" 12 + :baseSize="{w,h}"
13 :index="index" 13 :index="index"
14 /> 14 />
15 <span class="video-title">{{ item.name }}</span> 15 <span class="video-title">{{ item.name }}</span>
@@ -188,7 +188,7 @@ const handleMouseleave = () => (isShowSvg.value = false) @@ -188,7 +188,7 @@ const handleMouseleave = () => (isShowSvg.value = false)
188 left: 10%; 188 left: 10%;
189 transform: translateX(-50%); 189 transform: translateX(-50%);
190 transition: 0.5s; 190 transition: 0.5s;
191 - box-shadow: 0 0 4px black; 191 + // box-shadow: 0 0 4px black;
192 .video-title { 192 .video-title {
193 width: v-bind('w+"px"'); 193 width: v-bind('w+"px"');
194 font-size: 30px; 194 font-size: 30px;