Showing
17 changed files
with
398 additions
and
715 deletions
... | ... | @@ -72,14 +72,16 @@ |
72 | 72 | "sortablejs": "^1.14.0", |
73 | 73 | "tinymce": "^5.8.2", |
74 | 74 | "vditor": "^3.8.6", |
75 | - "video.js": "^7.20.3", | |
76 | - "videojs-flvjs-es6": "^1.0.1", | |
77 | 75 | "vue": "3.3.4", |
78 | 76 | "vue-i18n": "9.1.7", |
79 | 77 | "vue-json-pretty": "^2.0.4", |
80 | 78 | "vue-router": "^4.0.11", |
81 | 79 | "vue-types": "^4.0.3", |
82 | 80 | "vue3-grid-layout": "^1.0.0", |
81 | + "xgplayer": "^3.0.14", | |
82 | + "xgplayer-flv": "^3.0.14", | |
83 | + "xgplayer-hls": "^3.0.14", | |
84 | + "xgplayer-mp4": "^3.0.14", | |
83 | 85 | "xlsx": "^0.17.0" |
84 | 86 | }, |
85 | 87 | "devDependencies": { |
... | ... | @@ -101,7 +103,6 @@ |
101 | 103 | "@types/qrcode": "^1.4.1", |
102 | 104 | "@types/qs": "^6.9.7", |
103 | 105 | "@types/sortablejs": "^1.10.7", |
104 | - "@types/video.js": "^7.3.49", | |
105 | 106 | "@typescript-eslint/eslint-plugin": "^4.29.1", |
106 | 107 | "@typescript-eslint/parser": "^4.29.1", |
107 | 108 | "@vitejs/plugin-legacy": "^1.5.1", | ... | ... |
... | ... | @@ -109,7 +109,7 @@ export const deleteStreamingMediaRecord = (params: StreamingMediaDeleteParam) => |
109 | 109 | * @returns |
110 | 110 | */ |
111 | 111 | export const getStreamingPlayUrl = (entityId: string) => { |
112 | - return defHttp.get( | |
112 | + return defHttp.get<{ data: { url: string } }>( | |
113 | 113 | { |
114 | 114 | url: `${CameraManagerApi.STREAMING_PLAY_GET_URL}/${entityId}`, |
115 | 115 | }, | ... | ... |
... | ... | @@ -35,6 +35,7 @@ export interface CameraModel { |
35 | 35 | deviceId: string; |
36 | 36 | deviceName: string; |
37 | 37 | }; |
38 | + playProtocol?: number; | |
38 | 39 | } |
39 | 40 | |
40 | 41 | export interface StreamingManageRecord { |
... | ... | @@ -106,4 +107,14 @@ export interface CameraRecord { |
106 | 107 | channelNo: string; |
107 | 108 | deviceId: string; |
108 | 109 | }; |
110 | + videoPlatformDTO?: VideoPlatformDTO; | |
111 | +} | |
112 | + | |
113 | +interface VideoPlatformDTO { | |
114 | + enabled: boolean; | |
115 | + type: number; | |
116 | + host: string; | |
117 | + appKey: string; | |
118 | + appSecret: string; | |
119 | + ssl: number; | |
109 | 120 | } | ... | ... |
src/components/Video/src/VideoPlay.vue
deleted
100644 → 0
1 | -<script lang="ts" setup> | |
2 | - import { isNumber } from 'lodash'; | |
3 | - import videoJs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'; | |
4 | - import 'video.js/dist/video-js.css'; | |
5 | - import { computed, CSSProperties, onMounted, onUnmounted, ref, toRaw, unref } from 'vue'; | |
6 | - import { useDesign } from '/@/hooks/web/useDesign'; | |
7 | - import { getJwtToken, getShareJwtToken } from '/@/utils/auth'; | |
8 | - import { isShareMode } from '/@/views/sys/share/hook'; | |
9 | - import 'videojs-flvjs-es6'; | |
10 | - const { prefixCls } = useDesign('basic-video-play'); | |
11 | - | |
12 | - const props = withDefaults( | |
13 | - defineProps<{ | |
14 | - options?: VideoJsPlayerOptions; | |
15 | - withToken?: boolean; | |
16 | - immediateInitOnMounted?: boolean; | |
17 | - }>(), | |
18 | - { | |
19 | - immediateInitOnMounted: true, | |
20 | - } | |
21 | - ); | |
22 | - | |
23 | - const emit = defineEmits<{ | |
24 | - (event: 'ready', instance?: Nullable<VideoJsPlayer>): void; | |
25 | - (event: 'onUnmounted'): void; | |
26 | - }>(); | |
27 | - | |
28 | - const videoPlayEl = ref<HTMLVideoElement>(); | |
29 | - | |
30 | - const videoPlayInstance = ref<Nullable<VideoJsPlayer>>(); | |
31 | - | |
32 | - const getOptions = computed(() => { | |
33 | - const { options, withToken } = props; | |
34 | - | |
35 | - const defaultOptions: VideoJsPlayerOptions & Recordable = { | |
36 | - language: 'zh', | |
37 | - muted: true, | |
38 | - liveui: true, | |
39 | - controls: true, | |
40 | - techOrder: ['html5', 'flvjs'], | |
41 | - flvjs: { | |
42 | - mediaDataSource: { | |
43 | - isLive: true, | |
44 | - cors: true, | |
45 | - hasAudio: false, | |
46 | - withCredentials: false, | |
47 | - }, | |
48 | - config: { | |
49 | - headers: { | |
50 | - ...(withToken | |
51 | - ? { | |
52 | - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | |
53 | - } | |
54 | - : {}), | |
55 | - }, | |
56 | - autoCleanupSourceBuffer: true, | |
57 | - }, | |
58 | - }, | |
59 | - }; | |
60 | - return videoJs.mergeOptions(defaultOptions, options); | |
61 | - }); | |
62 | - | |
63 | - const getWidthHeight = computed(() => { | |
64 | - let { width = 300, height = 150 } = unref(getOptions); | |
65 | - width = isNumber(width) ? (`${width}px` as unknown as number) : width; | |
66 | - height = isNumber(height) ? (`${height}px` as unknown as number) : height; | |
67 | - return { width, height } as CSSProperties; | |
68 | - }); | |
69 | - | |
70 | - const init = () => { | |
71 | - if (unref(videoPlayInstance)) unref(videoPlayInstance)?.dispose(); | |
72 | - videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => { | |
73 | - emit('ready', unref(videoPlayInstance)); | |
74 | - }); | |
75 | - //fix 修复videojs解决直播延时的问题 | |
76 | - videoPlayInstance.value?.on('timeupdate', function () { | |
77 | - // 计算表最新推流的时间和现在播放器播放推流的时间 | |
78 | - let differTime = | |
79 | - (videoPlayInstance.value as any as VideoJsPlayer)?.buffered()?.end(0) - | |
80 | - (videoPlayInstance.value as any as VideoJsPlayer)?.currentTime(); | |
81 | - // 差值小于1.5s时根据1倍速进行播放 | |
82 | - if (differTime < 1.5) { | |
83 | - videoPlayInstance.value?.playbackRate(1); | |
84 | - } | |
85 | - // 差值大于1.5s小于10s根据1.2倍速进行播放 | |
86 | - if (differTime < 10 && differTime > 1.5) { | |
87 | - videoPlayInstance.value?.playbackRate(1.2); | |
88 | - } | |
89 | - // 差值大于10s时进行重新加载直播流 | |
90 | - if (differTime > 10) { | |
91 | - init(); | |
92 | - } | |
93 | - }); | |
94 | - // | |
95 | - }; | |
96 | - | |
97 | - const customInit = (getOptionsFn: (optios: VideoJsPlayerOptions) => VideoJsPlayerOptions) => { | |
98 | - return (videoPlayInstance.value = videoJs( | |
99 | - unref(videoPlayEl)!, | |
100 | - getOptionsFn(toRaw(unref(getOptions))), | |
101 | - () => { | |
102 | - emit('ready', unref(videoPlayInstance)); | |
103 | - } | |
104 | - )); | |
105 | - }; | |
106 | - | |
107 | - onMounted(() => { | |
108 | - props.immediateInitOnMounted && init(); | |
109 | - }); | |
110 | - | |
111 | - onUnmounted(() => { | |
112 | - unref(videoPlayInstance)?.dispose(); | |
113 | - videoPlayInstance.value = null; | |
114 | - emit('onUnmounted'); | |
115 | - }); | |
116 | - | |
117 | - defineExpose({ | |
118 | - customInit, | |
119 | - reloadPlayer: init, | |
120 | - getInstance: () => unref(videoPlayInstance), | |
121 | - }); | |
122 | -</script> | |
123 | - | |
124 | -<template> | |
125 | - <div :class="prefixCls" class="!w-full h-full" :style="getWidthHeight" style="min-height: 100%"> | |
126 | - <video | |
127 | - ref="videoPlayEl" | |
128 | - class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-full !h-full" | |
129 | - muted | |
130 | - > | |
131 | - </video> | |
132 | - </div> | |
133 | -</template> | |
134 | - | |
135 | -<style lang="less"> | |
136 | - @prefix-cls: ~'@{namespace}-basic-video-play'; | |
137 | - | |
138 | - .@{prefix-cls} { | |
139 | - .vjs-error-display { | |
140 | - .vjs-modal-dialog-content::after { | |
141 | - content: '无法加载视频,原因可能是服务器或网络故障,也可能是格式不支持.'; | |
142 | - } | |
143 | - } | |
144 | - } | |
145 | -</style> |
src/components/Video/src/XGPlayer.vue
0 → 100644
1 | +<script setup lang="ts"> | |
2 | + import Player, { Events, IError } from 'xgplayer'; | |
3 | + import { FlvPlugin } from 'xgplayer-flv'; | |
4 | + import Mp4Plugin from 'xgplayer-mp4'; | |
5 | + import { HlsPlugin } from 'xgplayer-hls'; | |
6 | + import { onMounted, shallowRef, computed, unref, toRaw, onUnmounted, ref, watch } from 'vue'; | |
7 | + import PresetPlayer from 'xgplayer'; | |
8 | + import { IPlayerOptions } from 'xgplayer/es/player'; | |
9 | + import 'xgplayer/dist/index.min.css'; | |
10 | + import { isShareMode } from '/@/views/sys/share/hook'; | |
11 | + import { getJwtToken, getShareJwtToken } from '/@/utils/auth'; | |
12 | + import { XGPlayerProps } from '/@/components/Video/src/types'; | |
13 | + import { StreamType } from './types'; | |
14 | + | |
15 | + const props = withDefaults(defineProps<XGPlayerProps>(), { | |
16 | + streamType: 'auto', | |
17 | + autoPlay: true, | |
18 | + config: () => ({}), | |
19 | + }); | |
20 | + | |
21 | + const emits = defineEmits<{ | |
22 | + (eventName: 'ready', player: PresetPlayer): void; | |
23 | + (eventName: 'onUnmounted', player: PresetPlayer): void; | |
24 | + }>(); | |
25 | + | |
26 | + function getStreamTypeByUrl(url = ''): StreamType | undefined { | |
27 | + if (url.endsWith('.m3u8')) return 'hls'; | |
28 | + else if (url.endsWith('.mp4')) return 'mp4'; | |
29 | + else if (url.endsWith('.flv')) { | |
30 | + return 'flv'; | |
31 | + } else return; | |
32 | + } | |
33 | + | |
34 | + const getPluginByStreamType = (): IPlayerOptions => { | |
35 | + let { url, withToken } = props; | |
36 | + let { streamType } = props; | |
37 | + streamType = streamType === 'auto' ? getStreamTypeByUrl(url)! : streamType; | |
38 | + | |
39 | + const liveConfig = { | |
40 | + targetLatency: 10, | |
41 | + maxLatency: 20, | |
42 | + disconnectTime: 0, | |
43 | + fetchOptions: withToken | |
44 | + ? { | |
45 | + headers: { | |
46 | + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | |
47 | + }, | |
48 | + } | |
49 | + : {}, | |
50 | + }; | |
51 | + const config: IPlayerOptions = { | |
52 | + flv: liveConfig, | |
53 | + hls: liveConfig, | |
54 | + }; | |
55 | + switch (streamType) { | |
56 | + case 'hls': | |
57 | + config.plugins = [HlsPlugin]; | |
58 | + break; | |
59 | + case 'mp4': | |
60 | + config.plugins = [Mp4Plugin]; | |
61 | + break; | |
62 | + case 'flv': | |
63 | + config.plugins = [FlvPlugin]; | |
64 | + break; | |
65 | + } | |
66 | + return config; | |
67 | + }; | |
68 | + | |
69 | + const videoElRef = shallowRef<Nullable<HTMLDivElement>>(); | |
70 | + | |
71 | + const playerRef = shallowRef<Nullable<PresetPlayer>>(); | |
72 | + | |
73 | + const propsRef = ref<XGPlayerProps>({}); | |
74 | + | |
75 | + const getPlayerConfig = computed<IPlayerOptions>(() => { | |
76 | + const { url, autoPlay, config } = props; | |
77 | + | |
78 | + const basicConfig: IPlayerOptions = { | |
79 | + ...config, | |
80 | + ...propsRef, | |
81 | + url, | |
82 | + lang: 'zh', | |
83 | + isLive: true, | |
84 | + autoplay: autoPlay, | |
85 | + autoplayMuted: autoPlay, | |
86 | + ...getPluginByStreamType(), | |
87 | + }; | |
88 | + return basicConfig; | |
89 | + }); | |
90 | + | |
91 | + function onDecodeError() { | |
92 | + console.warn('player happend decode error'); | |
93 | + playerRef.value?.destroy?.(); | |
94 | + initializePlayer(); | |
95 | + } | |
96 | + | |
97 | + function initializePlayer() { | |
98 | + if (unref(playerRef)) { | |
99 | + playerRef.value?.destroy?.(); | |
100 | + playerRef.value = null; | |
101 | + } | |
102 | + | |
103 | + const config = toRaw(unref(getPlayerConfig)); | |
104 | + | |
105 | + if (!unref(videoElRef)) return; | |
106 | + | |
107 | + const player = (playerRef.value = new Player(Object.assign(config, { el: unref(videoElRef) }))); | |
108 | + | |
109 | + player.on(Events.READY, () => { | |
110 | + emits('ready', player); | |
111 | + }); | |
112 | + | |
113 | + player.on(Events.ERROR, (error: IError) => { | |
114 | + // https://h5player.bytedance.com/api/error.html#type | |
115 | + if (error.errorCode === 5103) { | |
116 | + onDecodeError(); | |
117 | + } | |
118 | + }); | |
119 | + } | |
120 | + | |
121 | + onMounted(() => { | |
122 | + initializePlayer(); | |
123 | + }); | |
124 | + | |
125 | + onUnmounted(() => { | |
126 | + emits('onUnmounted', unref(playerRef)!); | |
127 | + playerRef.value?.destroy?.(); | |
128 | + }); | |
129 | + | |
130 | + watch( | |
131 | + () => props.url, | |
132 | + () => { | |
133 | + initializePlayer(); | |
134 | + } | |
135 | + ); | |
136 | + | |
137 | + defineExpose({ | |
138 | + getPlayerInstance: () => unref(playerRef), | |
139 | + }); | |
140 | +</script> | |
141 | + | |
142 | +<template> | |
143 | + <div ref="videoElRef"></div> | |
144 | +</template> | ... | ... |
src/components/Video/src/types.ts
0 → 100644
1 | +import { IPlayerOptions } from 'xgplayer/es/player'; | |
2 | + | |
3 | +export type StreamType = 'flv' | 'mp4' | 'hls' | 'auto'; | |
4 | +export interface XGPlayerProps { | |
5 | + streamType?: StreamType; | |
6 | + autoPlay?: boolean; | |
7 | + url?: string; | |
8 | + withToken?: boolean; | |
9 | + config?: Omit<IPlayerOptions, 'url'>; | |
10 | +} | ... | ... |
src/views/camera/manage/DialogPreviewVideo.vue
deleted
100644 → 0
1 | -<template> | |
2 | - <div> | |
3 | - <BasicModal | |
4 | - v-bind="$attrs" | |
5 | - width="55rem" | |
6 | - destroyOnClose | |
7 | - :height="heightNum" | |
8 | - @register="register" | |
9 | - title="视频预览" | |
10 | - :showOkBtn="false" | |
11 | - @cancel="handleCancel" | |
12 | - > | |
13 | - <div | |
14 | - class="flex items-center justify-center bg-dark-900 w-full h-full min-h-96 video-container" | |
15 | - > | |
16 | - <BasicVideoPlay | |
17 | - v-if="showVideo" | |
18 | - :options="(options as any)" | |
19 | - :withToken="withToken" | |
20 | - @on-unmounted="handleCloseFlvPlayUrl" | |
21 | - /> | |
22 | - </div> | |
23 | - </BasicModal> | |
24 | - </div> | |
25 | -</template> | |
26 | -<script setup lang="ts"> | |
27 | - import { ref, reactive, unref } from 'vue'; | |
28 | - import { BasicModal, useModalInner } from '/@/components/Modal'; | |
29 | - import type { StreamingManageRecord, CameraModel } from '/@/api/camera/model/cameraModel'; | |
30 | - import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; | |
31 | - import { AccessMode } from './config.data'; | |
32 | - import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | |
33 | - import { isRtspProtocol } from '/@/components/Video/src/utils'; | |
34 | - import { VideoJsPlayerOptions } from 'video.js'; | |
35 | - import { useFingerprint } from '/@/utils/useFingerprint'; | |
36 | - import { GetResult } from '@fingerprintjs/fingerprintjs'; | |
37 | - | |
38 | - const heightNum = ref(800); | |
39 | - const showVideo = ref(false); | |
40 | - | |
41 | - const playUrl = ref(''); | |
42 | - | |
43 | - const withToken = ref(false); | |
44 | - | |
45 | - const videoId = ref<string>(); | |
46 | - | |
47 | - const fingerprintResult = ref<Nullable<GetResult>>(null); | |
48 | - | |
49 | - const options = reactive<VideoJsPlayerOptions>({ | |
50 | - width: '100%' as unknown as number, | |
51 | - height: 384 as unknown as number, | |
52 | - autoplay: true, | |
53 | - }); | |
54 | - | |
55 | - const setSources = (url: string, fingerprintResult: GetResult) => { | |
56 | - const flag = isRtspProtocol(url); | |
57 | - options.sources = [ | |
58 | - { | |
59 | - src: flag ? getFlvPlayUrl(url, fingerprintResult.visitorId) : url, | |
60 | - type: getVideoTypeByUrl(url), | |
61 | - }, | |
62 | - ]; | |
63 | - }; | |
64 | - | |
65 | - const { getResult } = useFingerprint(); | |
66 | - const [register] = useModalInner( | |
67 | - async (data: { record: CameraModel | StreamingManageRecord }) => { | |
68 | - const { record } = data; | |
69 | - videoId.value = record.id || ''; | |
70 | - const result = await getResult(); | |
71 | - fingerprintResult.value = result; | |
72 | - if (record.accessMode === AccessMode.ManuallyEnter) { | |
73 | - if ((record as CameraModel).videoUrl) { | |
74 | - if (isRtspProtocol((record as CameraModel).videoUrl)) { | |
75 | - playUrl.value = (record as CameraModel).videoUrl; | |
76 | - closeFlvPlay(unref(playUrl), result.visitorId); | |
77 | - withToken.value = true; | |
78 | - } | |
79 | - setSources((record as CameraModel).videoUrl, result); | |
80 | - } | |
81 | - } else { | |
82 | - try { | |
83 | - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); | |
84 | - setSources(url, result); | |
85 | - } catch (error) {} | |
86 | - } | |
87 | - showVideo.value = true; | |
88 | - } | |
89 | - ); | |
90 | - | |
91 | - const handleCloseFlvPlayUrl = () => { | |
92 | - if (isRtspProtocol(unref(playUrl))) { | |
93 | - closeFlvPlay(unref(playUrl)!, unref(fingerprintResult)!.visitorId!); | |
94 | - } | |
95 | - }; | |
96 | - | |
97 | - const handleCancel = () => { | |
98 | - showVideo.value = false; | |
99 | - withToken.value = false; | |
100 | - }; | |
101 | -</script> | |
102 | - | |
103 | -<style lang="less" scoped> | |
104 | - .video-container:deep(.vben-basic-video-play) { | |
105 | - min-height: 13rem; | |
106 | - } | |
107 | - | |
108 | - .video-container:deep(.video-js) { | |
109 | - min-height: 13rem; | |
110 | - } | |
111 | -</style> |
... | ... | @@ -80,8 +80,7 @@ |
80 | 80 | </BasicTable> |
81 | 81 | </PageWrapper> |
82 | 82 | <CameraDrawer @register="registerDrawer" @success="handleSuccess" /> |
83 | - <VideoPreviewModal @register="registerModal" /> | |
84 | - <VideoModal @register="registerModal1" /> | |
83 | + <VideoModal @register="registerModal" /> | |
85 | 84 | <GBTDrawer @register="registerGBTDrawer" @success="handleGBTSuccess" /> |
86 | 85 | </div> |
87 | 86 | </template> |
... | ... | @@ -103,14 +102,16 @@ |
103 | 102 | CameraPermission, |
104 | 103 | VideoPlatformEnum, |
105 | 104 | accessModeConfig, |
105 | + getPlayUrl, | |
106 | 106 | } from './config.data'; |
107 | - import VideoPreviewModal from './DialogPreviewVideo.vue'; | |
108 | 107 | import { useModal } from '/@/components/Modal'; |
109 | 108 | import { Authority } from '/@/components/Authority'; |
110 | 109 | import { useBatchDelete } from '/@/hooks/web/useBatchDelete'; |
111 | 110 | import { Popconfirm } from 'ant-design-vue'; |
112 | 111 | import { Tag } from 'ant-design-vue'; |
113 | 112 | import VideoModal from '/@/views/device/list/cpns/tabs/VideoChannel/videoModal.vue'; |
113 | + import { VideoCancelModalParamsType } from '/@/views/device/list/cpns/tabs/VideoChannel/config'; | |
114 | + import { CameraRecord } from '/@/api/camera/model/cameraModel'; | |
114 | 115 | |
115 | 116 | export default defineComponent({ |
116 | 117 | components: { |
... | ... | @@ -119,7 +120,6 @@ |
119 | 120 | BasicTable, |
120 | 121 | TableAction, |
121 | 122 | CameraDrawer, |
122 | - VideoPreviewModal, | |
123 | 123 | VideoModal, |
124 | 124 | TableImg, |
125 | 125 | Authority, |
... | ... | @@ -131,9 +131,8 @@ |
131 | 131 | setup(_, { emit }) { |
132 | 132 | const searchInfo = reactive<Recordable>({}); |
133 | 133 | const { organizationIdTreeRef, resetFn } = useResetOrganizationTree(searchInfo); |
134 | - const [registerModal, { openModal }] = useModal(); //手动输入 | |
135 | 134 | |
136 | - const [registerModal1, { openModal: openModal1 }] = useModal(); //流媒体获取 | |
135 | + const [registerModal, { openModal }] = useModal(); //流媒体获取 | |
137 | 136 | // 表格hooks |
138 | 137 | const [registerTable, { reload, setProps, clearSelectedRowKeys }] = useTable({ |
139 | 138 | title: '视频列表', |
... | ... | @@ -197,28 +196,25 @@ |
197 | 196 | searchInfo.organizationId = organizationId; |
198 | 197 | handleSuccess(); |
199 | 198 | }; |
200 | - const handleViewVideo = (record) => { | |
201 | - const { videoPlatformDTO } = record; | |
199 | + const handleViewVideo = (record: CameraRecord) => { | |
200 | + const { videoPlatformDTO, params } = record; | |
202 | 201 | const { type } = videoPlatformDTO || {}; |
203 | - const commonRecord = { | |
204 | - isUpdate: true, | |
205 | - record, | |
206 | - }; | |
207 | - // 新增一个判断,GBT28181也弹出云台控制界面 | |
208 | - // if (record.accessMode === AccessMode.Streaming && type === VideoPlatformEnum.ISC) { | |
209 | - // openModal1(true, commonRecord); | |
210 | - // } else if (record.accessMode === AccessMode.GBT28181) { | |
211 | - // openModal1(true, { ...commonRecord, ifShowGBT: true }); | |
212 | - // } else openModal(true, commonRecord); | |
213 | 202 | |
214 | - if ( | |
215 | - record.accessMode === AccessMode.ManuallyEnter || | |
216 | - (record.accessMode === AccessMode.Streaming && type === VideoPlatformEnum.FLUORITE) | |
217 | - ) { | |
218 | - openModal(true, commonRecord); | |
219 | - } else if (record.accessMode === AccessMode.GBT28181) { | |
220 | - openModal1(true, { ...commonRecord, ifShowGBT: true }); | |
221 | - } else openModal1(true, commonRecord); | |
203 | + openModal(true, { | |
204 | + record: { | |
205 | + id: record.id, | |
206 | + canControl: | |
207 | + [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode) && | |
208 | + type !== VideoPlatformEnum.FLUORITE, | |
209 | + isGBT: record.accessMode === AccessMode.GBT28181, | |
210 | + channelId: params?.channelNo, | |
211 | + tbDeviceId: params?.deviceId, | |
212 | + getPlayUrl: async () => { | |
213 | + const result = await getPlayUrl(record); | |
214 | + return result; | |
215 | + }, | |
216 | + } as VideoCancelModalParamsType, | |
217 | + }); | |
222 | 218 | }; |
223 | 219 | |
224 | 220 | const handleSwitchMode = () => { |
... | ... | @@ -252,7 +248,6 @@ |
252 | 248 | organizationIdTreeRef, |
253 | 249 | handleViewVideo, |
254 | 250 | registerModal, |
255 | - registerModal1, | |
256 | 251 | AccessMode, |
257 | 252 | handleSwitchMode, |
258 | 253 | CameraPermission, | ... | ... |
... | ... | @@ -3,24 +3,21 @@ |
3 | 3 | import OrganizationIdTree from '../../common/organizationIdTree/src/OrganizationIdTree.vue'; |
4 | 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, closeFlvPlay, getFlvPlayUrl } from '/@/api/camera/cameraManager'; | |
6 | + import { cameraPage, closeFlvPlay } 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'; |
10 | 10 | import GBTDrawer from './components/GBTDrawer.vue'; |
11 | 11 | import { useDrawer } from '/@/components/Drawer'; |
12 | - import { AccessMode, CameraPermission, PageMode } from './config.data'; | |
12 | + import { CameraPermission, getPlayUrl, PageMode } from './config.data'; | |
13 | 13 | import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; |
14 | - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | |
15 | 14 | import { buildUUID } from '/@/utils/uuid'; |
16 | - import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; | |
17 | - import { VideoJsPlayerOptions } from 'video.js'; | |
18 | 15 | import { getBoundingClientRect } from '/@/utils/domUtils'; |
19 | 16 | import { Authority } from '/@/components/Authority'; |
20 | 17 | import { isRtspProtocol } from '/@/components/Video/src/utils'; |
21 | 18 | import { useFingerprint } from '/@/utils/useFingerprint'; |
22 | 19 | import { GetResult } from '@fingerprintjs/fingerprintjs'; |
23 | - import { getVideoControlStart } from '/@/api/device/videoChannel'; | |
20 | + import XGPlayer from '/@/components/Video/src/XGPlayer.vue'; | |
24 | 21 | |
25 | 22 | const props = defineProps({ |
26 | 23 | mode: { |
... | ... | @@ -29,17 +26,12 @@ |
29 | 26 | }, |
30 | 27 | }); |
31 | 28 | type CameraRecordItem = CameraRecord & { |
29 | + placeholder?: boolean; | |
32 | 30 | canPlay?: boolean; |
33 | 31 | isTransform?: boolean; |
34 | 32 | withToken?: boolean; |
35 | - videoPlayerOptions?: VideoJsPlayerOptions; | |
36 | 33 | playSourceUrl?: string; |
37 | - }; | |
38 | - | |
39 | - const basicVideoPlayOptions: VideoJsPlayerOptions = { | |
40 | - width: '100%' as unknown as number, | |
41 | - height: '100%' as unknown as number, | |
42 | - autoplay: true, | |
34 | + streamType?: string; | |
43 | 35 | }; |
44 | 36 | |
45 | 37 | const emit = defineEmits(['switchMode']); |
... | ... | @@ -88,10 +80,7 @@ |
88 | 80 | |
89 | 81 | for (const item of unref(cameraList)) { |
90 | 82 | (item as CameraRecordItem).isTransform = false; |
91 | - (item as CameraRecordItem).videoPlayerOptions = { | |
92 | - ...basicVideoPlayOptions, | |
93 | - }; | |
94 | - beforeVideoPlay(item as CameraRecordItem, result); | |
83 | + beforeVideoPlay(item as CameraRecordItem); | |
95 | 84 | } |
96 | 85 | } catch (error) { |
97 | 86 | } finally { |
... | ... | @@ -100,75 +89,11 @@ |
100 | 89 | }; |
101 | 90 | |
102 | 91 | const { getResult } = useFingerprint(); |
103 | - const beforeVideoPlay = async (record: CameraRecordItem, fingerprintResult: GetResult) => { | |
104 | - if (record.accessMode === AccessMode.ManuallyEnter) { | |
105 | - if (record.videoUrl) { | |
106 | - const isFlvPlay = isRtspProtocol(record.videoUrl); | |
107 | - const type = getVideoTypeByUrl(record.videoUrl); | |
108 | - record.playSourceUrl = record.videoUrl; | |
109 | - if (isFlvPlay) { | |
110 | - // handleFlvPlayerUnload(record, fingerprintResult!.visitorId); | |
111 | - record.playSourceUrl = getFlvPlayUrl(record.videoUrl, fingerprintResult.visitorId); | |
112 | - record.withToken = true; | |
113 | - } | |
114 | - | |
115 | - (record as CameraRecordItem).videoPlayerOptions = { | |
116 | - ...basicVideoPlayOptions, | |
117 | - sources: [ | |
118 | - { | |
119 | - src: record.playSourceUrl, | |
120 | - type, | |
121 | - }, | |
122 | - ], | |
123 | - }; | |
124 | - record.isTransform = true; | |
125 | - } | |
126 | - } | |
127 | - if (record.accessMode === AccessMode.GBT28181) { | |
128 | - try { | |
129 | - const { params } = record; | |
130 | - if (params?.channelNo && params.deviceId) { | |
131 | - const result = await getVideoControlStart({ | |
132 | - deviceId: params.deviceId, | |
133 | - channelId: params.channelNo, | |
134 | - }); | |
135 | - | |
136 | - (record as CameraRecordItem).videoPlayerOptions = { | |
137 | - ...basicVideoPlayOptions, | |
138 | - sources: [{ src: result.data.flv, type: getVideoTypeByUrl(result.data.flv) }], | |
139 | - }; | |
140 | - } | |
141 | - } finally { | |
142 | - record.isTransform = true; | |
143 | - } | |
144 | - } | |
145 | - if (record.accessMode === AccessMode.Streaming) { | |
146 | - try { | |
147 | - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(record.id!); | |
148 | - const index = unref(cameraList).findIndex((item) => item.id === record.id); | |
149 | - if (~index) { | |
150 | - const oldRecord = unref(cameraList).at(index)!; | |
151 | - unref(cameraList)[index] = { | |
152 | - ...oldRecord, | |
153 | - | |
154 | - videoPlayerOptions: { | |
155 | - ...basicVideoPlayOptions, | |
156 | - sources: [ | |
157 | - { | |
158 | - src: url, | |
159 | - type: getVideoTypeByUrl(url), | |
160 | - }, | |
161 | - ], | |
162 | - } as any, | |
163 | - isTransform: true, | |
164 | - }; | |
165 | - } | |
166 | - } catch (error) { | |
167 | - } finally { | |
168 | - const index = unref(cameraList).findIndex((item) => item.id === record.id); | |
169 | - if (~index) unref(cameraList)[index].isTransform = true; | |
170 | - } | |
171 | - } | |
92 | + const beforeVideoPlay = async (record: CameraRecordItem) => { | |
93 | + const { url, type } = await getPlayUrl(record); | |
94 | + record.playSourceUrl = url; | |
95 | + record.streamType = type; | |
96 | + record.isTransform = true; | |
172 | 97 | }; |
173 | 98 | |
174 | 99 | const gridLayout = ref({ gutter: 1, column: 2 }); |
... | ... | @@ -316,7 +241,7 @@ |
316 | 241 | :grid="(gridLayout as any)" |
317 | 242 | :style="{ '--height': `${100 / pagination.colNumber}%` }" |
318 | 243 | > |
319 | - <template #renderItem="{ item }"> | |
244 | + <template #renderItem="{ item }: { item: CameraRecordItem }"> | |
320 | 245 | <List.Item> |
321 | 246 | <div class="box-border w-full !h-full p-1px"> |
322 | 247 | <div |
... | ... | @@ -332,10 +257,12 @@ |
332 | 257 | v-show="!item.isTransform" |
333 | 258 | :spinning="!item.isTransform" |
334 | 259 | /> |
335 | - <BasicVideoPlay | |
260 | + <XGPlayer | |
336 | 261 | v-if="item.isTransform" |
337 | - :options="item.videoPlayerOptions" | |
262 | + :url="item.playSourceUrl" | |
263 | + :stream-type="item.streamType" | |
338 | 264 | :with-token="item.withToken" |
265 | + :config="{ width: '100%', height: '100%' }" | |
339 | 266 | @on-unmounted="handleCloseFlvPlayUrl(item)" |
340 | 267 | /> |
341 | 268 | <div | ... | ... |
... | ... | @@ -6,7 +6,6 @@ import { h } from 'vue'; |
6 | 6 | import SnHelpMessage from './SnHelpMessage.vue'; |
7 | 7 | import SnHelpMessage1 from './SnHelpMessage1.vue'; |
8 | 8 | import { OrgTreeSelect } from '../../common/OrgTreeSelect'; |
9 | -import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
10 | 9 | import { createImgPreview } from '/@/components/Preview'; |
11 | 10 | import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter'; |
12 | 11 | import { findDictItemByCode } from '/@/api/system/dict'; |
... | ... | @@ -15,7 +14,24 @@ import { DataSourceField } from '../../visual/packages/config/common.config'; |
15 | 14 | import { getMeetTheConditionsDevice } from '/@/api/dataBoard'; |
16 | 15 | import { useMessage } from '/@/hooks/web/useMessage'; |
17 | 16 | import { TransportTypeEnum } from '../../device/profiles/components/TransportDescript/const'; |
18 | -import { getDeviceChannelList } from '/@/api/camera/cameraManager'; | |
17 | +import { | |
18 | + getDeviceChannelList, | |
19 | + getFlvPlayUrl, | |
20 | + getStreamingPlayUrl, | |
21 | +} from '/@/api/camera/cameraManager'; | |
22 | +import { CameraRecord } from '/@/api/camera/model/cameraModel'; | |
23 | +import { isRtspProtocol } from '/@/components/Video/src/utils'; | |
24 | +import { useFingerprint } from '/@/utils/useFingerprint'; | |
25 | +import { getVideoControlStart } from '/@/api/device/videoChannel'; | |
26 | +import { StreamType as PlayerStreamType } from '/@/components/Video/src/types'; | |
27 | + | |
28 | +interface FileItem { | |
29 | + uid: string; | |
30 | + name: string; | |
31 | + status?: string; | |
32 | + response?: string; | |
33 | + url?: string; | |
34 | +} | |
19 | 35 | |
20 | 36 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
21 | 37 | |
... | ... | @@ -604,3 +620,37 @@ export const formGBTSchema: QFormSchema[] = [ |
604 | 620 | values.allDevice === GBT28181DeviceSelectMethod.PARTIAL, |
605 | 621 | }, |
606 | 622 | ]; |
623 | + | |
624 | +export async function getPlayUrl( | |
625 | + params: CameraRecord | |
626 | +): Promise<{ url: string; type: PlayerStreamType }> { | |
627 | + const { accessMode } = params; | |
628 | + if (accessMode === AccessMode.ManuallyEnter) { | |
629 | + const { videoUrl } = params; | |
630 | + if (params.videoUrl) { | |
631 | + const isRTSPPlay = isRtspProtocol(videoUrl); | |
632 | + | |
633 | + if (isRTSPPlay) { | |
634 | + const { getResult } = useFingerprint(); | |
635 | + const fingerprint = await getResult(); | |
636 | + return { url: getFlvPlayUrl(videoUrl, fingerprint.visitorId), type: 'flv' }; | |
637 | + } else { | |
638 | + return { url: videoUrl, type: 'auto' }; | |
639 | + } | |
640 | + } | |
641 | + } else if (accessMode === AccessMode.GBT28181) { | |
642 | + const { deviceId, channelNo } = params?.params || {}; | |
643 | + const result = await getVideoControlStart({ channelId: channelNo!, deviceId: deviceId! }); | |
644 | + return { url: result.data.flv, type: 'flv' }; | |
645 | + } else { | |
646 | + const { id, playProtocol } = params; | |
647 | + const result = await getStreamingPlayUrl(id); | |
648 | + const type: PlayerStreamType = | |
649 | + playProtocol === FluoriteMideaProtocolEnum.FLV | |
650 | + ? 'flv' | |
651 | + : playProtocol === FluoriteMideaProtocolEnum.HLS | |
652 | + ? 'hls' | |
653 | + : 'auto'; | |
654 | + return { url: result.data.url, type }; | |
655 | + } | |
656 | +} | ... | ... |
1 | 1 | import { h } from 'vue'; |
2 | 2 | import { BasicColumn, FormSchema } from '/@/components/Table'; |
3 | 3 | import { Tag } from 'ant-design-vue'; |
4 | -import { withInstall } from '/@/utils/index'; | |
5 | - | |
6 | -import VideoPlay from './video.vue'; | |
7 | -export const Video = withInstall(VideoPlay); | |
4 | +export type VideoCancelModalParamsType = { | |
5 | + canControl?: boolean; | |
6 | + isGBT?: boolean; | |
7 | + tbDeviceId?: string; | |
8 | + channelId?: string; | |
9 | + id?: string; | |
10 | + playerProps?: Recordable; | |
11 | + getPlayUrl: () => Promise<Record<'url' | 'type', string>>; | |
12 | +}; | |
8 | 13 | |
9 | 14 | //视频通道权限标识枚举 |
10 | 15 | export enum GBT28181_DEVICE_PERMISSION_ENUM { | ... | ... |
... | ... | @@ -8,7 +8,7 @@ |
8 | 8 | <a-button type="primary" @click="handleSyncPlay">通道同步</a-button> |
9 | 9 | </Authority> |
10 | 10 | </template> |
11 | - <template #hasAudio="{ record }"> | |
11 | + <template #hasAudio="{ record }: { record: VideoChannelItemType }"> | |
12 | 12 | <Switch |
13 | 13 | :checked="record.status === 1" |
14 | 14 | :loading="record.pendingStatus" |
... | ... | @@ -41,16 +41,22 @@ |
41 | 41 | </template> |
42 | 42 | |
43 | 43 | <script lang="ts" setup> |
44 | - import { configColumns, searchFormSchema, GBT28181_DEVICE_PERMISSION_ENUM } from './config'; | |
44 | + import { | |
45 | + configColumns, | |
46 | + searchFormSchema, | |
47 | + GBT28181_DEVICE_PERMISSION_ENUM, | |
48 | + VideoCancelModalParamsType, | |
49 | + } from './config'; | |
45 | 50 | import { BasicTable, useTable, TableAction } from '/@/components/Table'; |
46 | 51 | import { Switch } from 'ant-design-vue'; |
47 | 52 | import { DeviceRecord } from '/@/api/device/model/deviceModel'; |
48 | 53 | import VideoModal from './videoModal.vue'; |
49 | 54 | import { useModal } from '/@/components/Modal'; |
50 | 55 | import { useMessage } from '/@/hooks/web/useMessage'; |
51 | - import { getVideoChannelList } from '/@/api/device/videoChannel'; | |
56 | + import { getVideoChannelList, getVideoControlStart } from '/@/api/device/videoChannel'; | |
52 | 57 | import { stopOnDemandVideoApiGet, syncVideoApiGet } from '/@/api/camera/cameraManager'; |
53 | 58 | import { Authority } from '/@/components/Authority'; |
59 | + import { VideoChannelItemType } from '/@/api/device/model/videoChannelModel'; | |
54 | 60 | |
55 | 61 | const props = defineProps<{ deviceDetail: DeviceRecord }>(); |
56 | 62 | |
... | ... | @@ -91,10 +97,20 @@ |
91 | 97 | } |
92 | 98 | }; |
93 | 99 | |
94 | - const handlePlay = (record: Recordable) => { | |
100 | + const handlePlay = (record: VideoChannelItemType) => { | |
101 | + const { channelId } = record; | |
95 | 102 | openModal(true, { |
96 | - record, | |
97 | - ifShowGBT: true, | |
103 | + record: { | |
104 | + canControl: true, | |
105 | + isGBT: true, | |
106 | + channelId, | |
107 | + tbDeviceId: props.deviceDetail.tbDeviceId, | |
108 | + getPlayUrl: async () => { | |
109 | + const { deviceId, channelId } = record; | |
110 | + const result = await getVideoControlStart({ deviceId, channelId }); | |
111 | + return { type: 'flv', url: result.data.flv }; | |
112 | + }, | |
113 | + } as VideoCancelModalParamsType, | |
98 | 114 | }); |
99 | 115 | }; |
100 | 116 | ... | ... |
1 | 1 | <script lang="ts" setup> |
2 | - import { isNumber } from 'lodash'; | |
3 | - import videoJs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'; | |
4 | - import 'video.js/dist/video-js.css'; | |
5 | 2 | import { Tooltip } from 'ant-design-vue'; |
6 | - import { computed, CSSProperties, onMounted, onUnmounted, ref, unref } from 'vue'; | |
7 | - import { useDesign } from '/@/hooks/web/useDesign'; | |
8 | - import { getJwtToken, getShareJwtToken } from '/@/utils/auth'; | |
9 | - import { isShareMode } from '/@/views/sys/share/hook'; | |
10 | - import 'videojs-flvjs-es6'; | |
3 | + import { ref, unref } from 'vue'; | |
11 | 4 | import { QuestionCircleOutlined } from '@ant-design/icons-vue'; |
12 | 5 | import { |
13 | 6 | CaretUpOutlined, |
... | ... | @@ -19,106 +12,49 @@ |
19 | 12 | ZoomOutOutlined, |
20 | 13 | } from '@ant-design/icons-vue'; |
21 | 14 | import { Button, Slider } from 'ant-design-vue'; |
22 | - import { nextTick } from 'vue'; | |
23 | 15 | import { controlling } from '/@/api/camera/cameraManager'; |
24 | 16 | import { setVideoControl } from '/@/api/device/videoChannel'; |
25 | - | |
26 | - const { prefixCls } = useDesign('basic-video-play'); | |
27 | - interface IGbtOption { | |
28 | - tbDeviceId: string; | |
29 | - channelId: string; | |
30 | - } | |
17 | + import XGPlayer from '/@/components/Video/src/XGPlayer.vue'; | |
31 | 18 | |
32 | 19 | const props = defineProps<{ |
33 | - options?: VideoJsPlayerOptions; | |
34 | - withToken?: boolean; | |
35 | - isGBT?: boolean; | |
36 | - GBTOption: IGbtOption; | |
37 | - }>(); | |
38 | - | |
39 | - const emit = defineEmits<{ | |
40 | - (event: 'ready', instance?: Nullable<VideoJsPlayer>): void; | |
41 | - (event: 'onUnmounted'): void; | |
42 | - }>(); | |
43 | - | |
44 | - const videoPlayEl = ref<HTMLVideoElement>(); | |
45 | - | |
46 | - const videoPlayInstance = ref<Nullable<VideoJsPlayer>>(); | |
47 | - | |
48 | - const getGBT = computed(() => { | |
49 | - const { isGBT } = props; | |
50 | - return isGBT; | |
51 | - }); | |
52 | - | |
53 | - const getOptions = computed(() => { | |
54 | - const { options, withToken } = props; | |
55 | - | |
56 | - const defaultOptions: VideoJsPlayerOptions & Recordable = { | |
57 | - language: 'zh', | |
58 | - muted: true, | |
59 | - liveui: true, | |
60 | - controls: true, | |
61 | - techOrder: ['html5', 'flvjs'], | |
62 | - flvjs: { | |
63 | - mediaDataSource: { | |
64 | - isLive: true, | |
65 | - cors: true, | |
66 | - hasAudio: false, | |
67 | - withCredentials: false, | |
68 | - }, | |
69 | - config: { | |
70 | - headers: { | |
71 | - ...(withToken | |
72 | - ? { | |
73 | - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | |
74 | - } | |
75 | - : {}), | |
76 | - }, | |
77 | - autoCleanupSourceBuffer: true, | |
78 | - }, | |
79 | - }, | |
20 | + playUrl?: string; | |
21 | + options?: { | |
22 | + canControl?: boolean; | |
23 | + isGBT?: boolean; | |
24 | + tbDeviceId?: string; | |
25 | + channelId?: string; | |
26 | + id?: string; | |
27 | + playerProps?: Recordable; | |
80 | 28 | }; |
81 | - return videoJs.mergeOptions(defaultOptions, options); | |
82 | - }); | |
29 | + }>(); | |
83 | 30 | |
84 | - const getWidthHeight = computed(() => { | |
85 | - let { width = 300, height = 150 } = unref(getOptions); | |
86 | - width = isNumber(width) ? (`${width}px` as unknown as number) : width; | |
87 | - height = isNumber(height) ? (`${height}px` as unknown as number) : height; | |
88 | - return { width, height } as CSSProperties; | |
89 | - }); | |
31 | + const playerRef = ref<InstanceType<typeof XGPlayer>>(); | |
90 | 32 | |
91 | 33 | // 控制云台控制速度 |
92 | 34 | const sliderValue = ref<number>(123); |
93 | 35 | |
94 | - const init = () => { | |
95 | - if (unref(videoPlayInstance)) unref(videoPlayInstance)?.dispose(); | |
96 | - videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => { | |
97 | - emit('ready', unref(videoPlayInstance)); | |
98 | - }); | |
99 | - }; | |
36 | + const isPlay = ref(false); | |
100 | 37 | |
101 | 38 | //播放/暂停 |
102 | 39 | const handleClick = () => { |
103 | - // unref(isPlay) ((true,停止),(false,开启)) | |
104 | - if (unref(isPlay)) unref(videoPlayInstance)?.pause(); | |
105 | - else unref(videoPlayInstance)?.play(); | |
106 | - }; | |
107 | - | |
108 | - const getId = () => { | |
109 | - const { options } = props || {}; | |
110 | - const { sources }: any = options || {}; | |
111 | - return sources?.[0]?.id; | |
40 | + const playerInstance = unref(playerRef)?.getPlayerInstance(); | |
41 | + if (playerInstance && playerInstance.paused) { | |
42 | + unref(playerRef)?.getPlayerInstance()?.play(); | |
43 | + isPlay.value = true; | |
44 | + } else { | |
45 | + unref(playerRef)?.getPlayerInstance()?.pause(); | |
46 | + isPlay.value = false; | |
47 | + } | |
112 | 48 | }; |
113 | 49 | |
114 | 50 | const handleControl = (action: number, direction: string) => { |
115 | - const organizationId = getId(); | |
116 | - controlling({ cameraIndexCode: organizationId, action, command: direction }); | |
51 | + const { options } = props; | |
52 | + controlling({ cameraIndexCode: options?.id, action, command: direction }); | |
117 | 53 | }; |
118 | 54 | |
119 | 55 | // 国标控制 |
120 | 56 | const handleGBTControl = (command: string, action?: number | string) => { |
121 | - const { tbDeviceId, channelId } = props.GBTOption; | |
57 | + const { tbDeviceId, channelId } = props.options || {}; | |
122 | 58 | setVideoControl(tbDeviceId, channelId, { |
123 | 59 | command, |
124 | 60 | horizonSpeed: action, |
... | ... | @@ -127,33 +63,9 @@ |
127 | 63 | }); |
128 | 64 | }; |
129 | 65 | |
130 | - const isPlay = ref<Boolean | null | undefined>(false); | |
131 | - | |
132 | - onMounted(async () => { | |
133 | - init(); | |
134 | - await nextTick(); | |
135 | - // isPlay.value = unref(videoPlayInstance)?.paused(); | |
136 | - videoPlayInstance.value?.on('loadedmetadata', () => {}); | |
137 | - videoPlayInstance.value?.on('waiting', () => { | |
138 | - isPlay.value = false; | |
139 | - }); | |
140 | - videoPlayInstance.value?.on('play', () => { | |
141 | - isPlay.value = true; | |
142 | - }); | |
143 | - videoPlayInstance.value?.on('playing', () => { | |
144 | - isPlay.value = true; | |
145 | - }); | |
146 | - videoPlayInstance.value?.on('pause', () => { | |
147 | - isPlay.value = false; | |
148 | - }); | |
149 | - videoPlayInstance.value?.on('ended', () => { | |
150 | - isPlay.value = false; | |
151 | - }); | |
152 | - }); | |
153 | - | |
154 | 66 | //长按开始 |
155 | - const moveStart = (action) => { | |
156 | - if (unref(getGBT)) { | |
67 | + const moveStart = (action: string) => { | |
68 | + if (unref(props.options?.isGBT)) { | |
157 | 69 | handleGBTControl(action, unref(sliderValue)); |
158 | 70 | return; |
159 | 71 | } |
... | ... | @@ -161,46 +73,33 @@ |
161 | 73 | }; |
162 | 74 | |
163 | 75 | // 长按结束 |
164 | - const moveStop = (action) => { | |
165 | - if (unref(getGBT)) { | |
76 | + const moveStop = (action: string) => { | |
77 | + if (unref(props.options?.isGBT)) { | |
166 | 78 | handleGBTControl('STOP', unref(sliderValue)); |
167 | 79 | return; |
168 | 80 | } |
169 | 81 | handleControl(1, action); |
170 | 82 | }; |
171 | - | |
172 | - //页面卸载 | |
173 | - onUnmounted(() => { | |
174 | - unref(videoPlayInstance)?.dispose(); | |
175 | - videoPlayInstance.value = null; | |
176 | - emit('onUnmounted'); | |
177 | - }); | |
178 | - | |
179 | - defineExpose({ | |
180 | - reloadPlayer: init, | |
181 | - getInstance: () => unref(videoPlayInstance), | |
182 | - }); | |
183 | 83 | </script> |
184 | 84 | |
185 | 85 | <template> |
186 | - <div | |
187 | - :class="prefixCls" | |
188 | - class="!w-full h-full flex" | |
189 | - :style="getWidthHeight" | |
190 | - style="min-height: 100%" | |
191 | - > | |
192 | - <video | |
193 | - ref="videoPlayEl" | |
194 | - class="video-js vjs-big-play-centered vjs-show-big-play-button-on-pause !w-8/10 !h-full" | |
195 | - muted | |
86 | + <div class="!w-full h-full flex" style="min-height: 100%"> | |
87 | + <XGPlayer | |
88 | + ref="playerRef" | |
89 | + v-bind="options?.playerProps" | |
90 | + :url="playUrl" | |
91 | + auto-play | |
92 | + :config="{ fluid: true }" | |
93 | + /> | |
94 | + | |
95 | + <div | |
96 | + v-if="options?.canControl" | |
97 | + class="!w-2/10 px-4 bg-white flex justify-center items-center flex-col" | |
196 | 98 | > |
197 | - </video> | |
198 | - | |
199 | - <div class="!w-2/10 bg-white flex items-center flex-col"> | |
200 | 99 | <Tooltip> |
201 | - <template #title | |
202 | - >长按按钮调用API,由于视频流媒体提供的流,有一定时延,请耐心等待。</template | |
203 | - > | |
100 | + <template #title> | |
101 | + 长按按钮调用API,由于视频流媒体提供的流,有一定时延,请耐心等待。 | |
102 | + </template> | |
204 | 103 | <label class="validate-dot">云台控制</label> |
205 | 104 | <QuestionCircleOutlined :style="{ fontSize: '14px', marginLeft: '5px' }" /> |
206 | 105 | </Tooltip> |
... | ... | @@ -248,7 +147,7 @@ |
248 | 147 | <Button class="circle" @click="handleClick" /> |
249 | 148 | </div> |
250 | 149 | </div> |
251 | - <div class="mt-5" v-if="getGBT"> | |
150 | + <div class="mt-5" v-if="options.isGBT"> | |
252 | 151 | <Slider v-model:value="sliderValue" :min="0" :max="255" style="width: 130px" /> |
253 | 152 | </div> |
254 | 153 | <div class="flex justify-center mt-8"> |
... | ... | @@ -274,16 +173,6 @@ |
274 | 173 | </template> |
275 | 174 | |
276 | 175 | <style lang="less" scoped> |
277 | - @prefix-cls: ~'@{namespace}-basic-video-play'; | |
278 | - | |
279 | - .@{prefix-cls} { | |
280 | - .vjs-error-display { | |
281 | - .vjs-modal-dialog-content::after { | |
282 | - content: '无法加载视频,原因可能是服务器或网络故障,也可能是格式不支持.'; | |
283 | - } | |
284 | - } | |
285 | - } | |
286 | - | |
287 | 176 | .child { |
288 | 177 | position: absolute; |
289 | 178 | width: 3rem; | ... | ... |
1 | 1 | <template> |
2 | - <div> | |
3 | - <BasicModal | |
4 | - v-bind="$attrs" | |
5 | - width="60rem" | |
6 | - destroyOnClose | |
7 | - :height="heightNum" | |
8 | - @register="register" | |
9 | - title="视频预览" | |
10 | - :showOkBtn="false" | |
11 | - @cancel="handleCancel" | |
12 | - > | |
13 | - <div | |
14 | - class="flex items-center justify-center w-full h-full min-h-96 video-container bg-dark-50" | |
15 | - > | |
16 | - <VideoPlayer | |
17 | - ref="videoInstance" | |
18 | - v-if="showVideo" | |
19 | - :options="(options as VideoJsPlayerOptions)" | |
20 | - :withToken="withToken" | |
21 | - :isGBT="isGBT" | |
22 | - :GBTOption="GBTOption" | |
23 | - /> | |
24 | - </div> | |
25 | - </BasicModal> | |
26 | - </div> | |
2 | + <BasicModal | |
3 | + v-bind="$attrs" | |
4 | + width="60rem" | |
5 | + destroyOnClose | |
6 | + :height="800" | |
7 | + @register="register" | |
8 | + title="视频预览" | |
9 | + :showOkBtn="false" | |
10 | + @cancel="handleCancel" | |
11 | + > | |
12 | + <div class="flex items-center justify-center w-full h-full min-h-96 video-container bg-dark-50"> | |
13 | + <VideoPlayer :play-url="playUrl" :options="options" /> | |
14 | + </div> | |
15 | + </BasicModal> | |
27 | 16 | </template> |
28 | 17 | <script setup lang="ts"> |
29 | - import { ref, reactive } from 'vue'; | |
18 | + import { ref } from 'vue'; | |
30 | 19 | import { BasicModal, useModalInner } from '/@/components/Modal'; |
31 | - import { VideoJsPlayerOptions } from 'video.js'; | |
32 | 20 | import VideoPlayer from './video.vue'; |
33 | - import { getVideoControlStart } from '/@/api/device/videoChannel'; | |
34 | - import { VideoChannelItemType } from '/@/api/device/model/videoChannelModel'; | |
35 | - import { getVideoTypeByUrl } from '/@/components/Video'; | |
36 | - import { AccessMode } from '/@/views/camera/manage/config.data'; | |
37 | - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | |
38 | - | |
21 | + import { VideoCancelModalParamsType } from '/@/views/device/list/cpns/tabs/VideoChannel/config'; | |
39 | 22 | const emit = defineEmits(['reloadTable', 'register']); |
40 | 23 | |
41 | - const heightNum = ref(800); | |
42 | - const showVideo = ref(false); | |
43 | - | |
44 | - const videoInstance = ref<InstanceType<typeof VideoPlayer>>(); | |
45 | - | |
46 | - const videoId = ref<string>(); | |
47 | - | |
48 | - const withToken = ref(false); | |
49 | - | |
50 | - const options = reactive<VideoJsPlayerOptions>({ | |
51 | - width: '100%' as unknown as number, | |
52 | - height: 384 as unknown as number, | |
53 | - autoplay: true, | |
54 | - }); | |
55 | - const GBTOption = ref({ | |
56 | - tbDeviceId: '', | |
57 | - channelId: '', | |
58 | - }); | |
59 | - const isGBT = ref<boolean>(false); | |
60 | - | |
24 | + const playUrl = ref(); | |
25 | + const options = ref<VideoCancelModalParamsType>(); | |
61 | 26 | const [register, { setModalProps }] = useModalInner( |
62 | - async (data: { record: VideoChannelItemType }) => { | |
63 | - const { record, ifShowGBT = false } = data; | |
64 | - const { params } = record as Recordable; | |
65 | - videoId.value = record.id || ''; | |
66 | - isGBT.value = ifShowGBT; | |
67 | - GBTOption.value.tbDeviceId = params?.deviceId ? params?.deviceId : record?.deviceId; | |
68 | - GBTOption.value.channelId = params?.channelNo ? params?.channelNo : record?.channelId; | |
27 | + async (data: ModalParamsType<VideoCancelModalParamsType>) => { | |
28 | + const { record } = data; | |
69 | 29 | try { |
70 | 30 | setModalProps({ loading: true, loadingTip: '视频加载中...' }); |
71 | - // getStreamingPlayUrl | |
72 | - let result: any = null; | |
73 | - if (record.accessMode === AccessMode.GBT28181 || ifShowGBT) { | |
74 | - result = await getVideoControlStart({ | |
75 | - deviceId: params?.deviceId ? params?.deviceId : record?.deviceId, | |
76 | - channelId: params?.channelNo ? params?.channelNo : record?.channelId, | |
77 | - }); | |
78 | - options.sources = [{ src: result.data.flv, type: getVideoTypeByUrl(result.data.flv) }]; | |
79 | - } else { | |
80 | - result = await getStreamingPlayUrl(record.id); | |
81 | - options.sources = [ | |
82 | - { src: result.data.url, type: getVideoTypeByUrl(result.data.url), id: record.id }, | |
83 | - ]; | |
84 | - } | |
85 | - showVideo.value = true; | |
31 | + const { url, type } = await record.getPlayUrl(); | |
32 | + | |
33 | + playUrl.value = url; | |
34 | + options.value = record; | |
35 | + options.value.playerProps = { | |
36 | + ...(options.value?.playerProps || {}), | |
37 | + streamType: type, | |
38 | + }; | |
86 | 39 | } catch (error) { |
87 | 40 | } finally { |
88 | 41 | setModalProps({ loading: false }); |
... | ... | @@ -91,18 +44,6 @@ |
91 | 44 | ); |
92 | 45 | |
93 | 46 | const handleCancel = () => { |
94 | - showVideo.value = false; | |
95 | - withToken.value = false; | |
96 | 47 | emit('reloadTable'); |
97 | 48 | }; |
98 | 49 | </script> |
99 | - | |
100 | -<style lang="less" scoped> | |
101 | - .video-container:deep(.vben-basic-video-play) { | |
102 | - min-height: 13rem; | |
103 | - } | |
104 | - | |
105 | - .video-container:deep(.video-js) { | |
106 | - min-height: 13rem; | |
107 | - } | |
108 | -</style> | ... | ... |
... | ... | @@ -13,6 +13,7 @@ export enum FormFieldEnum { |
13 | 13 | |
14 | 14 | DEVICE_ID = 'deviceId', |
15 | 15 | CHANNEL_ID = 'channelId', |
16 | + PLAY_PROTOCOL = 'playProtocol', | |
16 | 17 | } |
17 | 18 | |
18 | 19 | export interface DataSourceValueType { |
... | ... | @@ -129,6 +130,7 @@ export const formSchemas: FormSchema[] = [ |
129 | 130 | accessMode === AccessMode.GBT28181 ? options.params?.channelNo : null, |
130 | 131 | [FormFieldEnum.DEVICE_ID]: |
131 | 132 | accessMode === AccessMode.GBT28181 ? options.params?.deviceId : null, |
133 | + [FormFieldEnum.PLAY_PROTOCOL]: options?.playProtocol, | |
132 | 134 | }); |
133 | 135 | }, |
134 | 136 | }; |
... | ... | @@ -146,4 +148,10 @@ export const formSchemas: FormSchema[] = [ |
146 | 148 | component: 'Input', |
147 | 149 | ifShow: false, |
148 | 150 | }, |
151 | + { | |
152 | + field: FormFieldEnum.PLAY_PROTOCOL, | |
153 | + label: '播放协议', | |
154 | + component: 'InputNumber', | |
155 | + ifShow: false, | |
156 | + }, | |
149 | 157 | ]; | ... | ... |
1 | 1 | <script lang="ts" setup> |
2 | 2 | import { ComponentMode, ComponentPropsConfigType } from '/@/views/visual/packages/index.type'; |
3 | 3 | import { option } from './config'; |
4 | - import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; | |
5 | - import { computed, onMounted, onUnmounted, toRaw } from 'vue'; | |
6 | - import { VideoJsPlayerOptions } from 'video.js'; | |
7 | - import { AccessMode } from '/@/views/camera/manage/config.data'; | |
8 | - import { unref } from 'vue'; | |
9 | - import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | |
4 | + import { XGPlayer } from '/@/components/Video'; | |
5 | + import { onMounted, onUnmounted } from 'vue'; | |
6 | + import { getPlayUrl } from '/@/views/camera/manage/config.data'; | |
7 | + import { closeFlvPlay } from '/@/api/camera/cameraManager'; | |
10 | 8 | import { ref } from 'vue'; |
11 | 9 | import { Spin } from 'ant-design-vue'; |
12 | 10 | import { useFingerprint } from '/@/utils/useFingerprint'; |
13 | - import { isRtspProtocol, VideoPlayerType } from '/@/components/Video/src/utils'; | |
14 | - import { isShareMode } from '/@/views/sys/share/hook'; | |
15 | - import { getJwtToken, getShareJwtToken } from '/@/utils/auth'; | |
16 | - import { getVideoControlStart } from '/@/api/device/videoChannel'; | |
11 | + import { isRtspProtocol } from '/@/components/Video/src/utils'; | |
12 | + import { CameraRecord } from '/@/api/camera/model/cameraModel'; | |
17 | 13 | |
18 | 14 | const props = defineProps<{ |
19 | 15 | config: ComponentPropsConfigType<typeof option>; |
... | ... | @@ -21,24 +17,12 @@ |
21 | 17 | |
22 | 18 | const loading = ref(true); |
23 | 19 | |
24 | - const basicVideoPlayEl = ref<Nullable<InstanceType<typeof BasicVideoPlay>>>(null); | |
25 | - | |
26 | - const withToken = ref(false); | |
27 | - | |
28 | - const playSource = ref<Record<'src' | 'type', string>>( | |
29 | - {} as unknown as Record<'src' | 'type', string> | |
30 | - ); | |
20 | + const playUrl = ref<string>(); | |
21 | + const playType = ref<string>(); | |
31 | 22 | |
32 | 23 | const exampleVideoPlay = |
33 | 24 | 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm'; |
34 | 25 | |
35 | - const getOptions = computed<VideoJsPlayerOptions>(() => { | |
36 | - return { | |
37 | - width: '100%' as unknown as number, | |
38 | - height: '100%' as unknown as number, | |
39 | - }; | |
40 | - }); | |
41 | - | |
42 | 26 | const { getResult } = useFingerprint(); |
43 | 27 | |
44 | 28 | const handleGetVideoPlayUrl = async () => { |
... | ... | @@ -46,45 +30,19 @@ |
46 | 30 | const { config } = props; |
47 | 31 | const { option } = config; |
48 | 32 | const { videoConfig, uuid } = option || {}; |
33 | + const { type, url } = await getPlayUrl({ | |
34 | + id: videoConfig?.id, | |
35 | + accessMode: videoConfig?.accessMode, | |
36 | + playProtocol: videoConfig?.playProtocol, | |
37 | + videoUrl: videoConfig?.videoUrl, | |
38 | + params: { | |
39 | + deviceId: videoConfig?.deviceId, | |
40 | + channelNo: videoConfig?.channelId, | |
41 | + }, | |
42 | + } as unknown as CameraRecord); | |
43 | + playType.value = type; | |
44 | + playUrl.value = url; | |
49 | 45 | if (!uuid) return; |
50 | - const { url, id, accessMode, deviceId, channelId } = videoConfig || {}; | |
51 | - let type = getVideoTypeByUrl(url!); | |
52 | - let playUrl = url; | |
53 | - if (accessMode === AccessMode.Streaming && id) { | |
54 | - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(id!); | |
55 | - playUrl = url; | |
56 | - playUrl && (type = getVideoTypeByUrl(playUrl!)); | |
57 | - } else if (accessMode === AccessMode.GBT28181 && deviceId && channelId) { | |
58 | - const { | |
59 | - data: { flv }, | |
60 | - } = await getVideoControlStart({ channelId, deviceId }); | |
61 | - | |
62 | - playUrl = flv; | |
63 | - type = VideoPlayerType.flv; | |
64 | - } | |
65 | - | |
66 | - if (isRtspProtocol(url!)) { | |
67 | - const result = await getResult(); | |
68 | - const { visitorId } = result; | |
69 | - playUrl = getFlvPlayUrl(playUrl!, visitorId); | |
70 | - withToken.value = true; | |
71 | - } | |
72 | - | |
73 | - playSource.value = { | |
74 | - src: playUrl!, | |
75 | - type, | |
76 | - }; | |
77 | - | |
78 | - const instance = unref(basicVideoPlayEl)?.customInit((options) => { | |
79 | - if (unref(withToken)) { | |
80 | - (options as any).flvjs.config.headers = { | |
81 | - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | |
82 | - }; | |
83 | - } | |
84 | - return { ...options, sources: [toRaw(unref(playSource))] } as VideoJsPlayerOptions; | |
85 | - }); | |
86 | - | |
87 | - instance?.play(); | |
88 | 46 | } finally { |
89 | 47 | loading.value = false; |
90 | 48 | } |
... | ... | @@ -92,20 +50,8 @@ |
92 | 50 | |
93 | 51 | const handleSelectPreview = () => { |
94 | 52 | loading.value = false; |
95 | - const instance = unref(basicVideoPlayEl)?.customInit((options) => { | |
96 | - withToken.value = true; | |
97 | - | |
98 | - if (unref(withToken)) { | |
99 | - (options as any).flvjs.config.headers = { | |
100 | - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | |
101 | - }; | |
102 | - } | |
103 | - return { | |
104 | - ...options, | |
105 | - sources: [{ type: getVideoTypeByUrl(exampleVideoPlay), src: exampleVideoPlay }], | |
106 | - } as VideoJsPlayerOptions; | |
107 | - }); | |
108 | - instance?.play(); | |
53 | + playUrl.value = exampleVideoPlay; | |
54 | + playType.value = 'auto'; | |
109 | 55 | }; |
110 | 56 | |
111 | 57 | onMounted(() => { |
... | ... | @@ -126,11 +72,10 @@ |
126 | 72 | <template> |
127 | 73 | <main class="w-full h-full flex flex-col justify-center items-center p-2"> |
128 | 74 | <Spin :spinning="loading" wrapper-class-name="video-spin"> |
129 | - <BasicVideoPlay | |
130 | - ref="basicVideoPlayEl" | |
131 | - :options="getOptions" | |
132 | - :with-token="withToken" | |
133 | - :immediateInitOnMounted="false" | |
75 | + <XGPlayer | |
76 | + :url="playUrl" | |
77 | + :stream-type="playType" | |
78 | + :config="{ width: '100%', height: '100%' }" | |
134 | 79 | /> |
135 | 80 | </Spin> |
136 | 81 | </main> | ... | ... |