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,6 +19,7 @@ | ||
| 19 | "dependencies": { | 19 | "dependencies": { | 
| 20 | "@amap/amap-jsapi-loader": "^1.0.1", | 20 | "@amap/amap-jsapi-loader": "^1.0.1", | 
| 21 | "@amap/amap-jsapi-types": "^0.0.8", | 21 | "@amap/amap-jsapi-types": "^0.0.8", | 
| 22 | + "@fingerprintjs/fingerprintjs": "^3.4.1", | ||
| 22 | "@types/color": "^3.0.3", | 23 | "@types/color": "^3.0.3", | 
| 23 | "@types/crypto-js": "^4.1.1", | 24 | "@types/crypto-js": "^4.1.1", | 
| 24 | "@types/keymaster": "^1.6.30", | 25 | "@types/keymaster": "^1.6.30", | 
| @@ -32,6 +33,7 @@ | @@ -32,6 +33,7 @@ | ||
| 32 | "echarts-liquidfill": "^3.1.0", | 33 | "echarts-liquidfill": "^3.1.0", | 
| 33 | "echarts-stat": "^1.2.0", | 34 | "echarts-stat": "^1.2.0", | 
| 34 | "echarts-wordcloud": "^2.0.0", | 35 | "echarts-wordcloud": "^2.0.0", | 
| 36 | + "flv.js": "^1.6.2", | ||
| 35 | "gsap": "^3.11.3", | 37 | "gsap": "^3.11.3", | 
| 36 | "highlight.js": "^11.5.0", | 38 | "highlight.js": "^11.5.0", | 
| 37 | "html2canvas": "^1.4.1", | 39 | "html2canvas": "^1.4.1", | 
| @@ -45,6 +47,7 @@ | @@ -45,6 +47,7 @@ | ||
| 45 | "screenfull": "^6.0.1", | 47 | "screenfull": "^6.0.1", | 
| 46 | "three": "^0.145.0", | 48 | "three": "^0.145.0", | 
| 47 | "video.js": "^7.20.3", | 49 | "video.js": "^7.20.3", | 
| 50 | + "videojs-flvjs-es6": "^1.0.1", | ||
| 48 | "vue": "^3.2.31", | 51 | "vue": "^3.2.31", | 
| 49 | "vue-3d-loader": "^2.1.7", | 52 | "vue-3d-loader": "^2.1.7", | 
| 50 | "vue-demi": "^0.13.1", | 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 | <template> | 1 | <template> | 
| 2 | <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }"> | 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 | </video> | 5 | </video> | 
| 8 | </div> | 6 | </div> | 
| 9 | </template> | 7 | </template> | 
| 10 | <script setup lang="ts"> | 8 | <script setup lang="ts"> | 
| 11 | -import { onMounted, ref, onUnmounted, watch } from 'vue' | 9 | +import { onMounted, ref, onUnmounted, watch, unref } from 'vue' | 
| 12 | import videojs from 'video.js' | 10 | import videojs from 'video.js' | 
| 11 | +import 'videojs-flvjs-es6' | ||
| 13 | import type { VideoJsPlayerOptions } from 'video.js' | 12 | import type { VideoJsPlayerOptions } from 'video.js' | 
| 14 | import 'video.js/dist/video-js.min.css' | 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 | const props = defineProps({ | 20 | const props = defineProps({ | 
| 17 | sourceSrc: { | 21 | sourceSrc: { | 
| @@ -36,40 +40,117 @@ const props = defineProps({ | @@ -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 | // video标签 | 73 | // video标签 | 
| 40 | const videoRef = ref<HTMLElement | null>(null) | 74 | const videoRef = ref<HTMLElement | null>(null) | 
| 41 | 75 | ||
| 42 | // video实例对象 | 76 | // video实例对象 | 
| 43 | let videoPlayer: videojs.Player | null = null | 77 | let videoPlayer: videojs.Player | null = null | 
| 44 | 78 | ||
| 79 | +const fingerprintResult = ref<Nullable<GetResult>>(null) | ||
| 80 | + | ||
| 45 | //options配置 | 81 | //options配置 | 
| 46 | -const options: VideoJsPlayerOptions = { | 82 | +const options: VideoJsPlayerOptions & Recordable = { | 
| 47 | language: 'zh-CN', // 设置语言 | 83 | language: 'zh-CN', // 设置语言 | 
| 48 | controls: true, // 是否显示控制条 | 84 | controls: true, // 是否显示控制条 | 
| 49 | preload: 'auto', // 预加载 | 85 | preload: 'auto', // 预加载 | 
| 50 | autoplay: true, // 是否自动播放 | 86 | autoplay: true, // 是否自动播放 | 
| 51 | fluid: false, // 自适应宽高 | 87 | fluid: false, // 自适应宽高 | 
| 52 | poster: props?.avatar || '', | 88 | poster: props?.avatar || '', | 
| 53 | - src: props?.sourceSrc || '', // 要嵌入的视频源的源 URL | 89 | + // src: getSource() || '', // 要嵌入的视频源的源 URL | 
| 90 | + sources: [], | ||
| 54 | muted: true, | 91 | muted: true, | 
| 55 | userActions: { | 92 | userActions: { | 
| 56 | hotkeys: true | 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 | // 初始化videojs | 126 | // 初始化videojs | 
| 61 | -const initVideo = () => { | 127 | +const initVideo = async () => { | 
| 62 | if (videoRef.value) { | 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 | watch( | 148 | watch( | 
| 69 | () => props.sourceSrc, | 149 | () => props.sourceSrc, | 
| 70 | - (newData: any) => { | 150 | + async (newData: any) => { | 
| 151 | + const result = await getSource() | ||
| 71 | // props.sourceSrc = newData | 152 | // props.sourceSrc = newData | 
| 72 | - videoPlayer?.src(newData) as any | 153 | + videoPlayer?.src(result) as any | 
| 73 | videoPlayer?.play() | 154 | videoPlayer?.play() | 
| 74 | }, | 155 | }, | 
| 75 | { | 156 | { | 
| @@ -82,6 +163,9 @@ onMounted(() => { | @@ -82,6 +163,9 @@ onMounted(() => { | ||
| 82 | }) | 163 | }) | 
| 83 | 164 | ||
| 84 | onUnmounted(() => { | 165 | onUnmounted(() => { | 
| 166 | + if (props.sourceSrc) { | ||
| 167 | + closeFlvPlay(props.sourceSrc, unref(fingerprintResult)!.visitorId!) | ||
| 168 | + } | ||
| 85 | handleVideoDispose() | 169 | handleVideoDispose() | 
| 86 | }) | 170 | }) | 
| 87 | 171 | ||
| @@ -101,6 +185,7 @@ defineExpose({ | @@ -101,6 +185,7 @@ defineExpose({ | ||
| 101 | display: flex; | 185 | display: flex; | 
| 102 | align-items: center; | 186 | align-items: center; | 
| 103 | justify-content: center; | 187 | justify-content: center; | 
| 188 | + | ||
| 104 | .my-video { | 189 | .my-video { | 
| 105 | width: 100% !important; | 190 | width: 100% !important; | 
| 106 | height: 100% !important; | 191 | height: 100% !important; | 
src/utils/external/useFingerprint.ts
0 → 100644