Commit 89d62489fc04f3ca7683ea296cf54dbd9f003dc1
Merge branch 'feat/video-component-support-rtsp-protocol' into 'main_dev'
Merge branch 'ww' into 'main_dev' See merge request yunteng/thingskit-view!80
Showing
4 changed files
with
131 additions
and
12 deletions
... | ... | @@ -19,6 +19,7 @@ |
19 | 19 | "dependencies": { |
20 | 20 | "@amap/amap-jsapi-loader": "^1.0.1", |
21 | 21 | "@amap/amap-jsapi-types": "^0.0.8", |
22 | + "@fingerprintjs/fingerprintjs": "^3.4.1", | |
22 | 23 | "@types/color": "^3.0.3", |
23 | 24 | "@types/crypto-js": "^4.1.1", |
24 | 25 | "@types/keymaster": "^1.6.30", |
... | ... | @@ -32,6 +33,7 @@ |
32 | 33 | "echarts-liquidfill": "^3.1.0", |
33 | 34 | "echarts-stat": "^1.2.0", |
34 | 35 | "echarts-wordcloud": "^2.0.0", |
36 | + "flv.js": "^1.6.2", | |
35 | 37 | "gsap": "^3.11.3", |
36 | 38 | "highlight.js": "^11.5.0", |
37 | 39 | "html2canvas": "^1.4.1", |
... | ... | @@ -45,6 +47,7 @@ |
45 | 47 | "screenfull": "^6.0.1", |
46 | 48 | "three": "^0.145.0", |
47 | 49 | "video.js": "^7.20.3", |
50 | + "videojs-flvjs-es6": "^1.0.1", | |
48 | 51 | "vue": "^3.2.31", |
49 | 52 | "vue-3d-loader": "^2.1.7", |
50 | 53 | "vue-demi": "^0.13.1", | ... | ... |
src/api/external/flvPlay/index.ts
0 → 100644
1 | +import { defHttp } from "@/utils/external/http/axios"; | |
2 | +import { useGlobSetting } from '@/hooks/external/setting'; | |
3 | +enum Api { | |
4 | + OPEN_FLV = '/rtsp/openFlv', | |
5 | + CLOSE_FLV = '/rtsp/closeFlv' | |
6 | +} | |
7 | + | |
8 | +export const getOpenFlvPlayUrl = (url: string, browserId: string) => { | |
9 | + const { urlPrefix, apiUrl } = useGlobSetting() | |
10 | + return `${apiUrl || ''}${urlPrefix || ''}${Api.OPEN_FLV}?url=${encodeURIComponent(url)}&browserId=${browserId}`; | |
11 | +}; | |
12 | + | |
13 | +export const closeFlvPlay = (url: string, browserId: string) => { | |
14 | + return defHttp.get({ | |
15 | + url: Api.CLOSE_FLV, | |
16 | + params: { | |
17 | + url: encodeURIComponent(url), | |
18 | + browserId | |
19 | + } | |
20 | + }); | |
21 | +}; | ... | ... |
1 | 1 | <template> |
2 | 2 | <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }"> |
3 | - <video | |
4 | - crossOrigin="anonymous" | |
5 | - :id="`my-player${index}`" ref="videoRef" class="video-js my-video vjs-theme-city vjs-big-play-centered"> | |
6 | - <source :src="sourceSrc" /> | |
3 | + <video crossOrigin="anonymous" :id="`my-player${index}`" ref="videoRef" | |
4 | + class="video-js my-video vjs-theme-city vjs-big-play-centered"> | |
7 | 5 | </video> |
8 | 6 | </div> |
9 | 7 | </template> |
10 | 8 | <script setup lang="ts"> |
11 | -import { onMounted, ref, onUnmounted, watch } from 'vue' | |
9 | +import { onMounted, ref, onUnmounted, watch, unref } from 'vue' | |
12 | 10 | import videojs from 'video.js' |
11 | +import 'videojs-flvjs-es6' | |
13 | 12 | import type { VideoJsPlayerOptions } from 'video.js' |
14 | 13 | import 'video.js/dist/video-js.min.css' |
14 | +import { getJwtToken, getShareJwtToken } from '@/utils/external/auth' | |
15 | +import { isShareMode } from '@/views/share/hook' | |
16 | +import { getOpenFlvPlayUrl, closeFlvPlay } from '@/api/external/flvPlay' | |
17 | +import { useFingerprint } from '@/utils/external/useFingerprint' | |
18 | +import { GetResult } from '@fingerprintjs/fingerprintjs' | |
15 | 19 | |
16 | 20 | const props = defineProps({ |
17 | 21 | sourceSrc: { |
... | ... | @@ -36,40 +40,117 @@ const props = defineProps({ |
36 | 40 | } |
37 | 41 | }) |
38 | 42 | |
43 | +enum VideoPlayerType { | |
44 | + m3u8 = 'application/x-mpegURL', | |
45 | + mp4 = 'video/mp4', | |
46 | + webm = 'video/webm', | |
47 | + flv = 'video/x-flv', | |
48 | +} | |
49 | + | |
50 | +const isRtspProtocol = (url: string) => { | |
51 | + const reg = /^rtsp:\/\//g; | |
52 | + return reg.test(url); | |
53 | +}; | |
54 | + | |
55 | +const getVideoTypeByUrl = (url = '') => { | |
56 | + try { | |
57 | + const { protocol, pathname } = new URL(url) | |
58 | + | |
59 | + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv | |
60 | + | |
61 | + const reg = /[^.]\w*$/ | |
62 | + const mathValue = pathname.match(reg) || [] | |
63 | + const ext = mathValue[0] as keyof typeof VideoPlayerType || 'webm' | |
64 | + const type = VideoPlayerType[ext] | |
65 | + return type ? type : VideoPlayerType.webm | |
66 | + } catch (error) { | |
67 | + console.error(error) | |
68 | + return VideoPlayerType.webm | |
69 | + } | |
70 | +}; | |
71 | + | |
72 | + | |
39 | 73 | // video标签 |
40 | 74 | const videoRef = ref<HTMLElement | null>(null) |
41 | 75 | |
42 | 76 | // video实例对象 |
43 | 77 | let videoPlayer: videojs.Player | null = null |
44 | 78 | |
79 | +const fingerprintResult = ref<Nullable<GetResult>>(null) | |
80 | + | |
45 | 81 | //options配置 |
46 | -const options: VideoJsPlayerOptions = { | |
82 | +const options: VideoJsPlayerOptions & Recordable = { | |
47 | 83 | language: 'zh-CN', // 设置语言 |
48 | 84 | controls: true, // 是否显示控制条 |
49 | 85 | preload: 'auto', // 预加载 |
50 | 86 | autoplay: true, // 是否自动播放 |
51 | 87 | fluid: false, // 自适应宽高 |
52 | 88 | poster: props?.avatar || '', |
53 | - src: props?.sourceSrc || '', // 要嵌入的视频源的源 URL | |
89 | + // src: getSource() || '', // 要嵌入的视频源的源 URL | |
90 | + sources: [], | |
54 | 91 | muted: true, |
55 | 92 | userActions: { |
56 | 93 | hotkeys: true |
94 | + }, | |
95 | + techOrder: ['html5', 'flvjs'], | |
96 | + flvjs: { | |
97 | + mediaDataSource: { | |
98 | + isLive: true, | |
99 | + cors: true, | |
100 | + withCredentials: false, | |
101 | + hasAudio: false, | |
102 | + autoCleanupSourceBuffer: true, | |
103 | + autoCleanupMaxBackwardDuration: 60, | |
104 | + }, | |
105 | + }, | |
106 | +} | |
107 | + | |
108 | + | |
109 | +const { getResult } = useFingerprint() | |
110 | +async function getSource() { | |
111 | + fingerprintResult.value = await getResult() | |
112 | + let src = props.sourceSrc || '' | |
113 | + | |
114 | + if (isRtspProtocol(props.sourceSrc!)) { | |
115 | + src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '') | |
57 | 116 | } |
117 | + | |
118 | + return [ | |
119 | + { | |
120 | + type: getVideoTypeByUrl(props.sourceSrc), | |
121 | + src | |
122 | + } | |
123 | + ] | |
58 | 124 | } |
59 | 125 | |
60 | 126 | // 初始化videojs |
61 | -const initVideo = () => { | |
127 | +const initVideo = async () => { | |
62 | 128 | if (videoRef.value) { |
63 | - // 创建 video 实例 | |
64 | - videoPlayer = videojs(videoRef.value, options) | |
129 | + // 创建 video 实例 | |
130 | + options.sources = await getSource() | |
131 | + if (options.sources && options.sources.length) { | |
132 | + | |
133 | + if (isRtspProtocol(props.sourceSrc || '')) { | |
134 | + options.flvjs = { | |
135 | + ...(options.flvjs || {}), | |
136 | + config: { | |
137 | + headers: { | |
138 | + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | |
139 | + } | |
140 | + } | |
141 | + } | |
142 | + } | |
143 | + videoPlayer = videojs(videoRef.value, options) | |
144 | + } | |
65 | 145 | } |
66 | 146 | } |
67 | 147 | |
68 | 148 | watch( |
69 | 149 | () => props.sourceSrc, |
70 | - (newData: any) => { | |
150 | + async (newData: any) => { | |
151 | + const result = await getSource() | |
71 | 152 | // props.sourceSrc = newData |
72 | - videoPlayer?.src(newData) as any | |
153 | + videoPlayer?.src(result) as any | |
73 | 154 | videoPlayer?.play() |
74 | 155 | }, |
75 | 156 | { |
... | ... | @@ -82,6 +163,9 @@ onMounted(() => { |
82 | 163 | }) |
83 | 164 | |
84 | 165 | onUnmounted(() => { |
166 | + if (props.sourceSrc) { | |
167 | + closeFlvPlay(props.sourceSrc, unref(fingerprintResult)!.visitorId!) | |
168 | + } | |
85 | 169 | handleVideoDispose() |
86 | 170 | }) |
87 | 171 | |
... | ... | @@ -101,6 +185,7 @@ defineExpose({ |
101 | 185 | display: flex; |
102 | 186 | align-items: center; |
103 | 187 | justify-content: center; |
188 | + | |
104 | 189 | .my-video { |
105 | 190 | width: 100% !important; |
106 | 191 | height: 100% !important; | ... | ... |
src/utils/external/useFingerprint.ts
0 → 100644