Commit d9e4ec706d6970a67dd2d2a474f9ea65afbb6040

Authored by xp.Huang
2 parents 5d526eda 8518abd2

Merge branch 'feat/video-component-support-rtsp-protocol' into 'main'

Feat/video component support rtsp protocol

See merge request yunteng/thingskit-front!651
... ... @@ -7,15 +7,21 @@
7 7 "cSpell.words": [
8 8 "COAP",
9 9 "edrx",
10   - "EFENTO",
  10 + "EFENTO",
  11 + "fingerprintjs",
  12 + "flvjs",
  13 + "flvjs",
11 14 "inited",
  15 + "liveui",
12 16 "MQTT",
13 17 "notif",
14 18 "PROTOBUF",
  19 + "rtsp",
15 20 "SCADA",
16 21 "SNMP",
17 22 "unref",
18 23 "vben",
  24 + "videojs",
19 25 "VITE",
20 26 "windicss"
21 27 ]
... ...
... ... @@ -35,6 +35,7 @@
35 35 "gen:iconfont": "esno ./build/generate/iconfont/index.ts"
36 36 },
37 37 "dependencies": {
  38 + "@fingerprintjs/fingerprintjs": "^3.4.1",
38 39 "@iconify/iconify": "^2.0.3",
39 40 "@logicflow/core": "^0.6.9",
40 41 "@logicflow/extension": "^0.6.9",
... ... @@ -49,6 +50,7 @@
49 50 "cropperjs": "^1.5.12",
50 51 "crypto-js": "^4.1.1",
51 52 "echarts": "^5.1.2",
  53 + "flv.js": "^1.6.2",
52 54 "hls.js": "^1.0.10",
53 55 "intro.js": "^4.1.0",
54 56 "jsoneditor": "^9.7.2",
... ... @@ -65,6 +67,7 @@
65 67 "tinymce": "^5.8.2",
66 68 "vditor": "^3.8.6",
67 69 "video.js": "^7.20.3",
  70 + "videojs-flvjs-es6": "^1.0.1",
68 71 "vue": "3.2.31",
69 72 "vue-i18n": "9.1.7",
70 73 "vue-json-pretty": "^2.0.4",
... ...
... ... @@ -101,3 +101,13 @@ export const getStreamingPlayUrl = (entityId: string) => {
101 101 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`,
102 102 });
103 103 };
  104 +
  105 +export const getFlvPlayUrl = (url: string, browserId: string) => {
  106 + return `/api/yt/rtsp/openFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`;
  107 +};
  108 +
  109 +export const closeFlvPlay = (url: string, browserId: string) => {
  110 + return defHttp.get({
  111 + url: `/rtsp/closeFlv?url=${encodeURIComponent(url)}&browserId=${browserId}`,
  112 + });
  113 +};
... ...
... ... @@ -4,15 +4,19 @@
4 4 import 'video.js/dist/video-js.css';
5 5 import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';
6 6 import { useDesign } from '/@/hooks/web/useDesign';
7   -
  7 + import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
  8 + import { isShareMode } from '/@/views/sys/share/hook';
  9 + import 'videojs-flvjs-es6';
8 10 const { prefixCls } = useDesign('basic-video-play');
9 11
10 12 const props = defineProps<{
11 13 options?: VideoJsPlayerOptions;
  14 + withToken?: boolean;
12 15 }>();
13 16
14 17 const emit = defineEmits<{
15 18 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;
  19 + (event: 'onUnmounted'): void;
16 20 }>();
17 21
18 22 const videoPlayEl = ref<HTMLVideoElement>();
... ... @@ -20,13 +24,34 @@
20 24 const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();
21 25
22 26 const getOptions = computed(() => {
23   - const { options } = props;
24   - const defaultOptions: VideoJsPlayerOptions = {
  27 + const { options, withToken } = props;
  28 + const defaultOptions: VideoJsPlayerOptions & Recordable = {
25 29 language: 'zh',
26 30 muted: true,
27 31 liveui: true,
28 32 controls: true,
  33 + techOrder: ['html5', 'flvjs'],
  34 + flvjs: {
  35 + mediaDataSource: {
  36 + isLive: true,
  37 + cors: true,
  38 + hasAudio: false,
  39 + withCredentials: false,
  40 + autoCleanupSourceBuffer: true,
  41 + autoCleanupMaxBackwardDuration: 60,
  42 + },
  43 + config: {
  44 + headers: {
  45 + ...(withToken
  46 + ? {
  47 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,
  48 + }
  49 + : {}),
  50 + },
  51 + },
  52 + },
29 53 };
  54 +
30 55 return { ...defaultOptions, ...options };
31 56 });
32 57
... ... @@ -50,6 +75,7 @@
50 75 onUnmounted(() => {
51 76 unref(videoPlayInstance)?.dispose();
52 77 videoPlayInstance.value = null;
  78 + emit('onUnmounted');
53 79 });
54 80 </script>
55 81
... ... @@ -58,7 +84,9 @@
58 84 <video
59 85 ref="videoPlayEl"
60 86 class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-full !h-full"
61   - ></video>
  87 + muted
  88 + >
  89 + </video>
62 90 </div>
63 91 </template>
64 92
... ...
1   -export enum VideoPlayerType {
2   - m3u8 = 'application/x-mpegURL',
3   - mp4 = 'video/mp4',
4   - webm = 'video/webm',
5   -}
6   -
7   -export const getVideoTypeByUrl = (url: string) => {
8   - const splitExtReg = /(?:.*)(?<=\.)/;
9   - const type = url.replace(splitExtReg, '');
10   - /**
11   - * https://vcsplay.scjtonline.cn:8200/live/HD_1569b634-4789-11eb-ab67-3cd2e55e0b20.m3u8?auth_key=1681179278-0-0-5c54a376f2ca32d05c4a152ee96336e9
12   - * 如果是这种格式的m3u8,则截取的是这一部分.m3u8?auth_key=1681179278-0-0-5c54a376f2ca32d05c4a152ee96336e9
13   - */
14   - if (type.startsWith('m3u8')) return VideoPlayerType.m3u8;
15   - if (type.startsWith('mp4')) return VideoPlayerType.mp4;
16   - if (type.startsWith('webm')) return VideoPlayerType.webm;
17   - return;
18   -};
  1 +export enum VideoPlayerType {
  2 + m3u8 = 'application/x-mpegURL',
  3 + mp4 = 'video/mp4',
  4 + webm = 'video/webm',
  5 + flv = 'video/x-flv',
  6 +}
  7 +
  8 +export const isRtspProtocol = (url: string) => {
  9 + const reg = /^rtsp:\/\//g;
  10 + return reg.test(url);
  11 +};
  12 +
  13 +export const getVideoTypeByUrl = (url: string) => {
  14 + try {
  15 + const { protocol, pathname } = new URL(url);
  16 + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv;
  17 +
  18 + const reg = /[^.]\w*$/;
  19 + const mathValue = pathname.match(reg) || [];
  20 + const ext = (mathValue[0] as keyof typeof VideoPlayerType) || 'webm';
  21 + const type = VideoPlayerType[ext];
  22 + return type ? type : VideoPlayerType.webm;
  23 + } catch (error) {
  24 + console.error(error);
  25 + return VideoPlayerType.webm;
  26 + }
  27 +};
... ...
  1 +import { load } from '@fingerprintjs/fingerprintjs';
  2 +export const useFingerprint = () => {
  3 + const getResult = async () => {
  4 + const fp = await load();
  5 + const result = await fp.get();
  6 + return result;
  7 + };
  8 +
  9 + return { getResult };
  10 +};
... ...
... ... @@ -11,58 +11,89 @@
11 11 @cancel="handleCancel"
12 12 >
13 13 <div
14   - class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52 video-container"
  14 + class="flex items-center justify-center bg-dark-900 w-full h-full min-h-96 video-container"
15 15 >
16   - <BasicVideoPlay v-if="showVideo" :options="options" />
  16 + <BasicVideoPlay
  17 + v-if="showVideo"
  18 + :options="(options as any)"
  19 + :withToken="withToken"
  20 + @on-unmounted="handleCloseFlvPlayUrl"
  21 + />
17 22 </div>
18 23 </BasicModal>
19 24 </div>
20 25 </template>
21 26 <script setup lang="ts">
22   - import { ref, reactive } from 'vue';
  27 + import { ref, reactive, unref } from 'vue';
23 28 import { BasicModal, useModalInner } from '/@/components/Modal';
24 29 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';
25 30 import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
26 31 import { AccessMode } from './config.data';
27   - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager';
  32 + import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager';
  33 + import { isRtspProtocol } from '/@/components/Video/src/utils';
28 34 import { VideoJsPlayerOptions } from 'video.js';
  35 + import { useFingerprint } from '/@/utils/useFingerprint';
  36 + import { GetResult } from '@fingerprintjs/fingerprintjs';
29 37
30 38 const heightNum = ref(800);
31 39 const showVideo = ref(false);
  40 +
  41 + const playUrl = ref('');
  42 +
  43 + const withToken = ref(false);
  44 +
  45 + const fingerprintResult = ref<Nullable<GetResult>>(null);
  46 +
32 47 const options = reactive<VideoJsPlayerOptions>({
33 48 width: '100%' as unknown as number,
34   - height: '100%' as unknown as number,
  49 + height: 384 as unknown as number,
35 50 autoplay: true,
36 51 });
37 52
38   - const setSources = (url: string) => {
  53 + const setSources = (url: string, fingerprintResult: GetResult) => {
  54 + const flag = isRtspProtocol(url);
39 55 options.sources = [
40 56 {
41   - src: url,
  57 + src: flag ? getFlvPlayUrl(url, fingerprintResult.visitorId) : url,
42 58 type: getVideoTypeByUrl(url),
43 59 },
44 60 ];
45 61 };
46 62
  63 + const { getResult } = useFingerprint();
47 64 const [register] = useModalInner(
48 65 async (data: { record: CameraModel | StreamingManageRecord }) => {
49 66 const { record } = data;
  67 + const result = await getResult();
  68 + fingerprintResult.value = result;
50 69 if (record.accessMode === AccessMode.ManuallyEnter) {
51 70 if ((record as CameraModel).videoUrl) {
52   - setSources((record as CameraModel).videoUrl);
  71 + if (isRtspProtocol((record as CameraModel).videoUrl)) {
  72 + playUrl.value = (record as CameraModel).videoUrl;
  73 + closeFlvPlay(unref(playUrl), result.visitorId);
  74 + withToken.value = true;
  75 + }
  76 + setSources((record as CameraModel).videoUrl, result);
53 77 }
54 78 } else {
55 79 try {
56 80 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
57   - setSources(url);
  81 + setSources(url, result);
58 82 } catch (error) {}
59 83 }
60 84 showVideo.value = true;
61 85 }
62 86 );
63 87
  88 + const handleCloseFlvPlayUrl = () => {
  89 + if (isRtspProtocol(unref(playUrl))) {
  90 + closeFlvPlay(unref(playUrl)!, unref(fingerprintResult)!.visitorId!);
  91 + }
  92 + };
  93 +
64 94 const handleCancel = () => {
65 95 showVideo.value = false;
  96 + withToken.value = false;
66 97 };
67 98 </script>
68 99
... ...
1 1 <script setup lang="ts">
2 2 import { PageWrapper } from '/@/components/Page';
3 3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
4   - import { onMounted, reactive, ref, unref, watch } from 'vue';
  4 + import { onMounted, reactive, Ref, ref, unref, watch } from 'vue';
5 5 import { Spin, Button, Pagination, Space, List } from 'ant-design-vue';
6   - import { cameraPage } from '/@/api/camera/cameraManager';
  6 + import { cameraPage, closeFlvPlay, getFlvPlayUrl } from '/@/api/camera/cameraManager';
7 7 import { CameraRecord } from '/@/api/camera/model/cameraModel';
8 8 import { useFullscreen } from '@vueuse/core';
9 9 import CameraDrawer from './CameraDrawer.vue';
... ... @@ -16,11 +16,16 @@
16 16 import { VideoJsPlayerOptions } from 'video.js';
17 17 import { getBoundingClientRect } from '/@/utils/domUtils';
18 18 import { Authority } from '/@/components/Authority';
  19 + import { isRtspProtocol } from '/@/components/Video/src/utils';
  20 + import { useFingerprint } from '/@/utils/useFingerprint';
  21 + import { GetResult } from '@fingerprintjs/fingerprintjs';
19 22
20 23 type CameraRecordItem = CameraRecord & {
21 24 canPlay?: boolean;
22 25 isTransform?: boolean;
  26 + withToken?: boolean;
23 27 videoPlayerOptions?: VideoJsPlayerOptions;
  28 + playSourceUrl?: string;
24 29 };
25 30
26 31 const basicVideoPlayOptions: VideoJsPlayerOptions = {
... ... @@ -43,6 +48,8 @@
43 48 total: 0,
44 49 });
45 50
  51 + const fingerprintResult = ref<Nullable<GetResult>>(null);
  52 +
46 53 // 树形选择器
47 54 const handleSelect = (orgId: string) => {
48 55 organizationId.value = orgId;
... ... @@ -60,12 +67,14 @@
60 67 });
61 68 pagination.total = total;
62 69
  70 + const result = await getResult();
  71 + fingerprintResult.value = result;
63 72 for (const item of items) {
64 73 (item as CameraRecordItem).isTransform = false;
65 74 (item as CameraRecordItem).videoPlayerOptions = {
66 75 ...basicVideoPlayOptions,
67 76 };
68   - beforeVideoPlay(item);
  77 + beforeVideoPlay(item, result);
69 78 }
70 79 if (items.length < pagination.pageSize) {
71 80 const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({
... ... @@ -81,15 +90,25 @@
81 90 }
82 91 };
83 92
84   - const beforeVideoPlay = async (record: CameraRecordItem) => {
  93 + const { getResult } = useFingerprint();
  94 + const beforeVideoPlay = async (record: CameraRecordItem, fingerprintResult: GetResult) => {
85 95 if (record.accessMode === AccessMode.ManuallyEnter) {
86 96 if (record.videoUrl) {
  97 + const isFlvPlay = isRtspProtocol(record.videoUrl);
  98 + const type = getVideoTypeByUrl(record.videoUrl);
  99 + record.playSourceUrl = record.videoUrl;
  100 + if (isFlvPlay) {
  101 + // handleFlvPlayerUnload(record, fingerprintResult!.visitorId);
  102 + record.playSourceUrl = getFlvPlayUrl(record.videoUrl, fingerprintResult.visitorId);
  103 + record.withToken = true;
  104 + }
  105 +
87 106 (record as CameraRecordItem).videoPlayerOptions = {
88 107 ...basicVideoPlayOptions,
89 108 sources: [
90 109 {
91   - src: record.videoUrl,
92   - type: getVideoTypeByUrl(record.videoUrl),
  110 + src: record.playSourceUrl,
  111 + type,
93 112 },
94 113 ],
95 114 };
... ... @@ -104,6 +123,7 @@
104 123 const oldRecord = unref(cameraList).at(index)!;
105 124 unref(cameraList)[index] = {
106 125 ...oldRecord,
  126 +
107 127 videoPlayerOptions: {
108 128 ...basicVideoPlayOptions,
109 129 sources: [
... ... @@ -112,7 +132,7 @@
112 132 type: getVideoTypeByUrl(url),
113 133 },
114 134 ],
115   - },
  135 + } as any,
116 136 isTransform: true,
117 137 };
118 138 }
... ... @@ -134,7 +154,7 @@
134 154 getCameraList();
135 155 };
136 156
137   - const { enter, isFullscreen } = useFullscreen(videoContainer);
  157 + const { enter, isFullscreen } = useFullscreen(videoContainer as Ref<HTMLDivElement>);
138 158
139 159 const handleFullScreen = () => {
140 160 enter();
... ... @@ -160,6 +180,12 @@
160 180 });
161 181 };
162 182
  183 + const handleCloseFlvPlayUrl = async (record: CameraRecordItem) => {
  184 + if (isRtspProtocol(record.videoUrl)) {
  185 + closeFlvPlay(record.videoUrl, unref(fingerprintResult)!.visitorId!);
  186 + }
  187 + };
  188 +
163 189 onMounted(() => {
164 190 getCameraList();
165 191 });
... ... @@ -244,7 +270,7 @@
244 270 :loading="loading"
245 271 :data-source="cameraList"
246 272 class="bg-light-50 w-full h-full dark:bg-dark-900 split-mode-list"
247   - :grid="gridLayout"
  273 + :grid="(gridLayout as any)"
248 274 :style="{ '--height': `${100 / pagination.colNumber}%` }"
249 275 >
250 276 <template #renderItem="{ item }">
... ... @@ -263,7 +289,12 @@
263 289 v-show="!item.isTransform"
264 290 :spinning="!item.isTransform"
265 291 />
266   - <BasicVideoPlay v-if="item.isTransform" :options="item.videoPlayerOptions" />
  292 + <BasicVideoPlay
  293 + v-if="item.isTransform"
  294 + :options="item.videoPlayerOptions"
  295 + :with-token="item.withToken"
  296 + @on-unmounted="handleCloseFlvPlayUrl(item)"
  297 + />
267 298 <div
268 299 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center"
269 300 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"
... ... @@ -316,6 +347,10 @@
316 347 .split-mode-list:deep(.ant-row) {
317 348 width: 100%;
318 349 height: 100%;
  350 +
  351 + > div {
  352 + height: var(--height);
  353 + }
319 354 }
320 355
321 356 .split-mode-list:deep(.ant-list-item) {
... ...