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> |
...
|
...
|
|