Commit 232fe75790d26cdb5fe28643d2eeba404e54d939

Authored by loveumiko
1 parent 77820111

fix: 修复多个视频播放loading问题

1 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 12 </template>
11 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 15 import videojs from 'video.js'
14 16 import 'videojs-flvjs-es6'
15 17 import type { VideoJsPlayerOptions } from 'video.js'
16 18 import 'video.js/dist/video-js.min.css'
17 19 import { getJwtToken, getShareJwtToken } from '@/utils/external/auth'
18 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 22 import { useFingerprint } from '@/utils/external/useFingerprint'
21 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 27 const props = defineProps({
25 28 sourceSrc: {
26 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 38 autoPlay: {
29 39 type: Boolean
30 40 },
... ... @@ -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 74 const isRtspProtocol = (url: string) => {
51 75 const reg = /^rtsp:\/\//g
52 76 return reg.test(url)
53 77 }
54 78
55 79 const getVideoTypeByUrl = (url = '') => {
56   - if(!url) return;
  80 + if (!url) return;
57 81 try {
58 82 const { protocol, pathname } = new URL(url)
59 83 if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv
... ... @@ -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 96 //options配置
80 97 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 124 async function getSource() {
109 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 128 src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '')
113 129 }
114 130 return [
115 131 {
116   - type: getVideoTypeByUrl(props.sourceSrc),
  132 + type: getVideoTypeByUrl(src),
117 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 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 248 watch(
190 249 () => props.autoPlay,
... ... @@ -192,13 +251,15 @@ watch(
192 251 if (newData) {
193 252 handleVideoPlay()
194 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 265 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 278 </script>
218 279
219 280 <style lang="scss" scoped>
  281 +
220 282 .go-content-box {
221 283 display: flex;
222 284 align-items: center;
... ... @@ -229,3 +291,8 @@ const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
229 291 }
230 292 }
231 293 </style>
  294 +<style>
  295 +.vjs-poster {
  296 + background-size: 100% !important;
  297 +}
  298 +</style>
... ...
... ... @@ -64,7 +64,8 @@ export const option = {
64 64 url: null,
65 65 sourceType: sourceTypeEnum.CUSTOM,
66 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 176
177 177 const handleSelect = (_: string, e: videoList, index: number) => {
178 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 185 onMounted(() => {
... ...
... ... @@ -4,12 +4,12 @@
4 4 <div v-for="(item, index) in option.dataset" :key="index" :class="item.className" :style="item.sty">
5 5 <VideoPlay
6 6 :autoPlay="item.autoPlay"
  7 + :option="item"
7 8 :name="item.name"
8 9 :avatar="item.avatar"
9 10 :key="item + index"
10 11 :sourceSrc="item.url"
11   - :w="w"
12   - :h="h"
  12 + :baseSize="{w,h}"
13 13 :index="index"
14 14 />
15 15 <span class="video-title">{{ item.name }}</span>
... ... @@ -188,7 +188,7 @@ const handleMouseleave = () => (isShowSvg.value = false)
188 188 left: 10%;
189 189 transform: translateX(-50%);
190 190 transition: 0.5s;
191   - box-shadow: 0 0 4px black;
  191 + // box-shadow: 0 0 4px black;
192 192 .video-title {
193 193 width: v-bind('w+"px"');
194 194 font-size: 30px;
... ...