Commit 51190fdd2232d45ba5d9a0c3f98d43b7aec77899

Authored by ww
1 parent 0f31669d

feat: 实现视频组件rtsp协议播放

@@ -8,12 +8,15 @@ @@ -8,12 +8,15 @@
8 "COAP", 8 "COAP",
9 "edrx", 9 "edrx",
10 "EFENTO", 10 "EFENTO",
  11 + "fingerprintjs",
  12 + "flvjs",
11 "flvjs", 13 "flvjs",
12 "inited", 14 "inited",
13 "liveui", 15 "liveui",
14 "MQTT", 16 "MQTT",
15 "notif", 17 "notif",
16 - "PROTOBUF", 18 + "PROTOBUF",
  19 + "rtsp",
17 "SCADA", 20 "SCADA",
18 "SNMP", 21 "SNMP",
19 "unref", 22 "unref",
@@ -35,6 +35,7 @@ @@ -35,6 +35,7 @@
35 "gen:iconfont": "esno ./build/generate/iconfont/index.ts" 35 "gen:iconfont": "esno ./build/generate/iconfont/index.ts"
36 }, 36 },
37 "dependencies": { 37 "dependencies": {
  38 + "@fingerprintjs/fingerprintjs": "^3.4.1",
38 "@iconify/iconify": "^2.0.3", 39 "@iconify/iconify": "^2.0.3",
39 "@logicflow/core": "^0.6.9", 40 "@logicflow/core": "^0.6.9",
40 "@logicflow/extension": "^0.6.9", 41 "@logicflow/extension": "^0.6.9",
@@ -101,3 +101,13 @@ export const getStreamingPlayUrl = (entityId: string) => { @@ -101,3 +101,13 @@ export const getStreamingPlayUrl = (entityId: string) => {
101 url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`, 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,15 +4,19 @@
4 import 'video.js/dist/video-js.css'; 4 import 'video.js/dist/video-js.css';
5 import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue'; 5 import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue';
6 import { useDesign } from '/@/hooks/web/useDesign'; 6 import { useDesign } from '/@/hooks/web/useDesign';
  7 + import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
  8 + import { isShareMode } from '/@/views/sys/share/hook';
7 import 'videojs-flvjs-es6'; 9 import 'videojs-flvjs-es6';
8 const { prefixCls } = useDesign('basic-video-play'); 10 const { prefixCls } = useDesign('basic-video-play');
9 11
10 const props = defineProps<{ 12 const props = defineProps<{
11 options?: VideoJsPlayerOptions; 13 options?: VideoJsPlayerOptions;
  14 + withToken?: boolean;
12 }>(); 15 }>();
13 16
14 const emit = defineEmits<{ 17 const emit = defineEmits<{
15 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void; 18 (event: 'ready', instance?: Nullable<VideoJsPlayer>): void;
  19 + (event: 'onUnmounted'): void;
16 }>(); 20 }>();
17 21
18 const videoPlayEl = ref<HTMLVideoElement>(); 22 const videoPlayEl = ref<HTMLVideoElement>();
@@ -20,7 +24,7 @@ @@ -20,7 +24,7 @@
20 const videoPlayInstance = ref<Nullable<VideoJsPlayer>>(); 24 const videoPlayInstance = ref<Nullable<VideoJsPlayer>>();
21 25
22 const getOptions = computed(() => { 26 const getOptions = computed(() => {
23 - const { options } = props; 27 + const { options, withToken } = props;
24 const defaultOptions: VideoJsPlayerOptions & Recordable = { 28 const defaultOptions: VideoJsPlayerOptions & Recordable = {
25 language: 'zh', 29 language: 'zh',
26 muted: true, 30 muted: true,
@@ -34,8 +38,18 @@ @@ -34,8 +38,18 @@
34 hasAudio: false, 38 hasAudio: false,
35 withCredentials: false, 39 withCredentials: false,
36 }, 40 },
  41 + config: {
  42 + headers: {
  43 + ...(withToken
  44 + ? {
  45 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,
  46 + }
  47 + : {}),
  48 + },
  49 + },
37 }, 50 },
38 }; 51 };
  52 +
39 return { ...defaultOptions, ...options }; 53 return { ...defaultOptions, ...options };
40 }); 54 });
41 55
@@ -59,6 +73,7 @@ @@ -59,6 +73,7 @@
59 onUnmounted(() => { 73 onUnmounted(() => {
60 unref(videoPlayInstance)?.dispose(); 74 unref(videoPlayInstance)?.dispose();
61 videoPlayInstance.value = null; 75 videoPlayInstance.value = null;
  76 + emit('onUnmounted');
62 }); 77 });
63 </script> 78 </script>
64 79
@@ -5,9 +5,15 @@ export enum VideoPlayerType { @@ -5,9 +5,15 @@ export enum VideoPlayerType {
5 flv = 'video/x-flv', 5 flv = 'video/x-flv',
6 } 6 }
7 7
  8 +export const isRtspProtocol = (url: string) => {
  9 + const reg = /^rtsp:\/\//g;
  10 + return reg.test(url);
  11 +};
  12 +
8 export const getVideoTypeByUrl = (url: string) => { 13 export const getVideoTypeByUrl = (url: string) => {
9 try { 14 try {
10 - const { pathname } = new URL(url); 15 + const { protocol, pathname } = new URL(url);
  16 + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv;
11 17
12 const reg = /[^.]\w*$/; 18 const reg = /[^.]\w*$/;
13 const mathValue = pathname.match(reg) || []; 19 const mathValue = pathname.match(reg) || [];
  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 +};
@@ -13,56 +13,87 @@ @@ -13,56 +13,87 @@
13 <div 13 <div
14 class="flex items-center justify-center bg-dark-900 w-full h-full min-h-96 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 as any)" /> 16 + <BasicVideoPlay
  17 + v-if="showVideo"
  18 + :options="(options as any)"
  19 + :withToken="withToken"
  20 + @on-unmounted="handleCloseFlvPlayUrl"
  21 + />
17 </div> 22 </div>
18 </BasicModal> 23 </BasicModal>
19 </div> 24 </div>
20 </template> 25 </template>
21 <script setup lang="ts"> 26 <script setup lang="ts">
22 - import { ref, reactive } from 'vue'; 27 + import { ref, reactive, unref } from 'vue';
23 import { BasicModal, useModalInner } from '/@/components/Modal'; 28 import { BasicModal, useModalInner } from '/@/components/Modal';
24 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel'; 29 import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel';
25 import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; 30 import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video';
26 import { AccessMode } from './config.data'; 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 import { VideoJsPlayerOptions } from 'video.js'; 34 import { VideoJsPlayerOptions } from 'video.js';
  35 + import { useFingerprint } from '/@/utils/useFingerprint';
  36 + import { GetResult } from '@fingerprintjs/fingerprintjs';
29 37
30 const heightNum = ref(800); 38 const heightNum = ref(800);
31 const showVideo = ref(false); 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 const options = reactive<VideoJsPlayerOptions>({ 47 const options = reactive<VideoJsPlayerOptions>({
33 width: '100%' as unknown as number, 48 width: '100%' as unknown as number,
34 height: 384 as unknown as number, 49 height: 384 as unknown as number,
35 autoplay: true, 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 options.sources = [ 55 options.sources = [
40 { 56 {
41 - src: url, 57 + src: flag ? getFlvPlayUrl(url, fingerprintResult.visitorId) : url,
42 type: getVideoTypeByUrl(url), 58 type: getVideoTypeByUrl(url),
43 }, 59 },
44 ]; 60 ];
45 }; 61 };
46 62
  63 + const { getResult } = useFingerprint();
47 const [register] = useModalInner( 64 const [register] = useModalInner(
48 async (data: { record: CameraModel | StreamingManageRecord }) => { 65 async (data: { record: CameraModel | StreamingManageRecord }) => {
49 const { record } = data; 66 const { record } = data;
  67 + const result = await getResult();
  68 + fingerprintResult.value = result;
50 if (record.accessMode === AccessMode.ManuallyEnter) { 69 if (record.accessMode === AccessMode.ManuallyEnter) {
51 if ((record as CameraModel).videoUrl) { 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 } else { 78 } else {
55 try { 79 try {
56 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); 80 const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!);
57 - setSources(url); 81 + setSources(url, result);
58 } catch (error) {} 82 } catch (error) {}
59 } 83 }
60 showVideo.value = true; 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 const handleCancel = () => { 94 const handleCancel = () => {
65 showVideo.value = false; 95 showVideo.value = false;
  96 + withToken.value = false;
66 }; 97 };
67 </script> 98 </script>
68 99
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue'; 3 import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue';
4 import { onMounted, reactive, Ref, ref, unref, watch } from 'vue'; 4 import { onMounted, reactive, Ref, ref, unref, watch } from 'vue';
5 import { Spin, Button, Pagination, Space, List } from 'ant-design-vue'; 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 import { CameraRecord } from '/@/api/camera/model/cameraModel'; 7 import { CameraRecord } from '/@/api/camera/model/cameraModel';
8 import { useFullscreen } from '@vueuse/core'; 8 import { useFullscreen } from '@vueuse/core';
9 import CameraDrawer from './CameraDrawer.vue'; 9 import CameraDrawer from './CameraDrawer.vue';
@@ -16,11 +16,16 @@ @@ -16,11 +16,16 @@
16 import { VideoJsPlayerOptions } from 'video.js'; 16 import { VideoJsPlayerOptions } from 'video.js';
17 import { getBoundingClientRect } from '/@/utils/domUtils'; 17 import { getBoundingClientRect } from '/@/utils/domUtils';
18 import { Authority } from '/@/components/Authority'; 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 type CameraRecordItem = CameraRecord & { 23 type CameraRecordItem = CameraRecord & {
21 canPlay?: boolean; 24 canPlay?: boolean;
22 isTransform?: boolean; 25 isTransform?: boolean;
  26 + withToken?: boolean;
23 videoPlayerOptions?: VideoJsPlayerOptions; 27 videoPlayerOptions?: VideoJsPlayerOptions;
  28 + playSourceUrl?: string;
24 }; 29 };
25 30
26 const basicVideoPlayOptions: VideoJsPlayerOptions = { 31 const basicVideoPlayOptions: VideoJsPlayerOptions = {
@@ -43,6 +48,8 @@ @@ -43,6 +48,8 @@
43 total: 0, 48 total: 0,
44 }); 49 });
45 50
  51 + const fingerprintResult = ref<Nullable<GetResult>>(null);
  52 +
46 // 树形选择器 53 // 树形选择器
47 const handleSelect = (orgId: string) => { 54 const handleSelect = (orgId: string) => {
48 organizationId.value = orgId; 55 organizationId.value = orgId;
@@ -60,12 +67,14 @@ @@ -60,12 +67,14 @@
60 }); 67 });
61 pagination.total = total; 68 pagination.total = total;
62 69
  70 + const result = await getResult();
  71 + fingerprintResult.value = result;
63 for (const item of items) { 72 for (const item of items) {
64 (item as CameraRecordItem).isTransform = false; 73 (item as CameraRecordItem).isTransform = false;
65 (item as CameraRecordItem).videoPlayerOptions = { 74 (item as CameraRecordItem).videoPlayerOptions = {
66 ...basicVideoPlayOptions, 75 ...basicVideoPlayOptions,
67 }; 76 };
68 - beforeVideoPlay(item); 77 + beforeVideoPlay(item, result);
69 } 78 }
70 if (items.length < pagination.pageSize) { 79 if (items.length < pagination.pageSize) {
71 const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({ 80 const fillArr: any = Array.from({ length: pagination.pageSize - items.length }).map(() => ({
@@ -81,16 +90,24 @@ @@ -81,16 +90,24 @@
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 if (record.accessMode === AccessMode.ManuallyEnter) { 95 if (record.accessMode === AccessMode.ManuallyEnter) {
86 if (record.videoUrl) { 96 if (record.videoUrl) {
  97 + const isFlvPlay = isRtspProtocol(record.videoUrl);
87 const type = getVideoTypeByUrl(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 + }
88 105
89 (record as CameraRecordItem).videoPlayerOptions = { 106 (record as CameraRecordItem).videoPlayerOptions = {
90 ...basicVideoPlayOptions, 107 ...basicVideoPlayOptions,
91 sources: [ 108 sources: [
92 { 109 {
93 - src: record.videoUrl, 110 + src: record.playSourceUrl,
94 type, 111 type,
95 }, 112 },
96 ], 113 ],
@@ -106,6 +123,7 @@ @@ -106,6 +123,7 @@
106 const oldRecord = unref(cameraList).at(index)!; 123 const oldRecord = unref(cameraList).at(index)!;
107 unref(cameraList)[index] = { 124 unref(cameraList)[index] = {
108 ...oldRecord, 125 ...oldRecord,
  126 +
109 videoPlayerOptions: { 127 videoPlayerOptions: {
110 ...basicVideoPlayOptions, 128 ...basicVideoPlayOptions,
111 sources: [ 129 sources: [
@@ -162,6 +180,12 @@ @@ -162,6 +180,12 @@
162 }); 180 });
163 }; 181 };
164 182
  183 + const handleCloseFlvPlayUrl = async (record: CameraRecordItem) => {
  184 + if (isRtspProtocol(record.videoUrl)) {
  185 + closeFlvPlay(record.videoUrl, unref(fingerprintResult)!.visitorId!);
  186 + }
  187 + };
  188 +
165 onMounted(() => { 189 onMounted(() => {
166 getCameraList(); 190 getCameraList();
167 }); 191 });
@@ -265,7 +289,12 @@ @@ -265,7 +289,12 @@
265 v-show="!item.isTransform" 289 v-show="!item.isTransform"
266 :spinning="!item.isTransform" 290 :spinning="!item.isTransform"
267 /> 291 />
268 - <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 + />
269 <div 298 <div
270 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center" 299 class="video-container-mask absolute top-0 left-0 z-50 text-lg w-full text-light-50 flex justify-center items-center"
271 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)" 300 style="height: 100%; background-color: rgba(0, 0, 0, 0.5)"