Commit 8cfa3384bd19fbceeff2bfc15d5e460393429387
Merge branch 'feat/data-board-video-component-support-rtsp-protocol' into 'main_dev'
feat: 数据看板视频组件支持rtsp协议播放 See merge request yunteng/thingskit-front!671
Showing
6 changed files
with
112 additions
and
54 deletions
| @@ -25,6 +25,7 @@ | @@ -25,6 +25,7 @@ | ||
| 25 | 25 | ||
| 26 | const getOptions = computed(() => { | 26 | const getOptions = computed(() => { |
| 27 | const { options, withToken } = props; | 27 | const { options, withToken } = props; |
| 28 | + | ||
| 28 | const defaultOptions: VideoJsPlayerOptions & Recordable = { | 29 | const defaultOptions: VideoJsPlayerOptions & Recordable = { |
| 29 | language: 'zh', | 30 | language: 'zh', |
| 30 | muted: true, | 31 | muted: true, |
| @@ -50,8 +51,7 @@ | @@ -50,8 +51,7 @@ | ||
| 50 | }, | 51 | }, |
| 51 | }, | 52 | }, |
| 52 | }; | 53 | }; |
| 53 | - | ||
| 54 | - return { ...defaultOptions, ...options }; | 54 | + return videoJs.mergeOptions(defaultOptions, options); |
| 55 | }); | 55 | }); |
| 56 | 56 | ||
| 57 | const getWidthHeight = computed(() => { | 57 | const getWidthHeight = computed(() => { |
| @@ -62,6 +62,7 @@ | @@ -62,6 +62,7 @@ | ||
| 62 | }); | 62 | }); |
| 63 | 63 | ||
| 64 | const init = () => { | 64 | const init = () => { |
| 65 | + if (unref(videoPlayInstance)) unref(videoPlayInstance)?.dispose(); | ||
| 65 | videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => { | 66 | videoPlayInstance.value = videoJs(unref(videoPlayEl)!, unref(getOptions), () => { |
| 66 | emit('ready', unref(videoPlayInstance)); | 67 | emit('ready', unref(videoPlayInstance)); |
| 67 | }); | 68 | }); |
| @@ -76,6 +77,11 @@ | @@ -76,6 +77,11 @@ | ||
| 76 | videoPlayInstance.value = null; | 77 | videoPlayInstance.value = null; |
| 77 | emit('onUnmounted'); | 78 | emit('onUnmounted'); |
| 78 | }); | 79 | }); |
| 80 | + | ||
| 81 | + defineExpose({ | ||
| 82 | + reloadPlayer: init, | ||
| 83 | + getInstance: () => unref(videoPlayInstance), | ||
| 84 | + }); | ||
| 79 | </script> | 85 | </script> |
| 80 | 86 | ||
| 81 | <template> | 87 | <template> |
| @@ -10,6 +10,7 @@ import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages | @@ -10,6 +10,7 @@ import { PublicConfigClass, componentInitConfig } from '/@/views/visual/packages | ||
| 10 | 10 | ||
| 11 | export const option: PublicPresetOptions = { | 11 | export const option: PublicPresetOptions = { |
| 12 | componetDesign: false, | 12 | componetDesign: false, |
| 13 | + trendQuery: false, | ||
| 13 | }; | 14 | }; |
| 14 | 15 | ||
| 15 | export default class Config extends PublicConfigClass implements CreateComponentType { | 16 | export default class Config extends PublicConfigClass implements CreateComponentType { |
| @@ -26,7 +26,7 @@ export enum VideoOriginalEnum { | @@ -26,7 +26,7 @@ export enum VideoOriginalEnum { | ||
| 26 | 26 | ||
| 27 | export enum VideoOriginalNameEnum { | 27 | export enum VideoOriginalNameEnum { |
| 28 | CUSTOM = '自定义', | 28 | CUSTOM = '自定义', |
| 29 | - VIDEO_MANAGE = '流媒体获取', | 29 | + VIDEO_MANAGE = '平台获取', |
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | export const formSchemas: FormSchema[] = [ | 32 | export const formSchemas: FormSchema[] = [ |
| @@ -36,15 +36,29 @@ export const formSchemas: FormSchema[] = [ | @@ -36,15 +36,29 @@ export const formSchemas: FormSchema[] = [ | ||
| 36 | label: '视频来源', | 36 | label: '视频来源', |
| 37 | colProps: { span: 8 }, | 37 | colProps: { span: 8 }, |
| 38 | defaultValue: VideoOriginalEnum.CUSTOM, | 38 | defaultValue: VideoOriginalEnum.CUSTOM, |
| 39 | - componentProps: { | ||
| 40 | - placeholder: '请选择视频来源', | ||
| 41 | - options: [ | ||
| 42 | - { label: VideoOriginalNameEnum[VideoOriginalEnum.CUSTOM], value: VideoOriginalEnum.CUSTOM }, | ||
| 43 | - { | ||
| 44 | - label: VideoOriginalNameEnum[VideoOriginalEnum.VIDEO_MANAGE], | ||
| 45 | - value: VideoOriginalEnum.VIDEO_MANAGE, | 39 | + componentProps: ({ formActionType }) => { |
| 40 | + const { setFieldsValue } = formActionType; | ||
| 41 | + return { | ||
| 42 | + placeholder: '请选择视频来源', | ||
| 43 | + options: [ | ||
| 44 | + { | ||
| 45 | + label: VideoOriginalNameEnum[VideoOriginalEnum.CUSTOM], | ||
| 46 | + value: VideoOriginalEnum.CUSTOM, | ||
| 47 | + }, | ||
| 48 | + { | ||
| 49 | + label: VideoOriginalNameEnum[VideoOriginalEnum.VIDEO_MANAGE], | ||
| 50 | + value: VideoOriginalEnum.VIDEO_MANAGE, | ||
| 51 | + }, | ||
| 52 | + ], | ||
| 53 | + onChange() { | ||
| 54 | + setFieldsValue({ | ||
| 55 | + [FormFieldEnum.ACCESS_MODE]: null, | ||
| 56 | + [FormFieldEnum.ORGANIZATION_ID]: null, | ||
| 57 | + [FormFieldEnum.VIDEO_PLAY_URL]: null, | ||
| 58 | + [FormFieldEnum.VIDEO_ID]: null, | ||
| 59 | + }); | ||
| 46 | }, | 60 | }, |
| 47 | - ], | 61 | + }; |
| 48 | }, | 62 | }, |
| 49 | }, | 63 | }, |
| 50 | { | 64 | { |
| @@ -69,9 +83,19 @@ export const formSchemas: FormSchema[] = [ | @@ -69,9 +83,19 @@ export const formSchemas: FormSchema[] = [ | ||
| 69 | label: '组织', | 83 | label: '组织', |
| 70 | colProps: { span: 8 }, | 84 | colProps: { span: 8 }, |
| 71 | ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.VIDEO_MANAGE, | 85 | ifShow: ({ model }) => model[FormFieldEnum.VIDEO_TYPE] === VideoOriginalEnum.VIDEO_MANAGE, |
| 72 | - componentProps: { | ||
| 73 | - showCreate: false, | ||
| 74 | - allowClean: true, | 86 | + componentProps: ({ formActionType }) => { |
| 87 | + const { setFieldsValue } = formActionType; | ||
| 88 | + return { | ||
| 89 | + showCreate: false, | ||
| 90 | + allowClean: true, | ||
| 91 | + onChange() { | ||
| 92 | + setFieldsValue({ | ||
| 93 | + [FormFieldEnum.ACCESS_MODE]: null, | ||
| 94 | + [FormFieldEnum.VIDEO_PLAY_URL]: null, | ||
| 95 | + [FormFieldEnum.VIDEO_ID]: null, | ||
| 96 | + }); | ||
| 97 | + }, | ||
| 98 | + }; | ||
| 75 | }, | 99 | }, |
| 76 | }, | 100 | }, |
| 77 | { | 101 | { |
| @@ -91,7 +115,6 @@ export const formSchemas: FormSchema[] = [ | @@ -91,7 +115,6 @@ export const formSchemas: FormSchema[] = [ | ||
| 91 | valueField: 'id', | 115 | valueField: 'id', |
| 92 | resultField: 'data', | 116 | resultField: 'data', |
| 93 | onChange(value: string, options: CameraModel) { | 117 | onChange(value: string, options: CameraModel) { |
| 94 | - console.log(options); | ||
| 95 | setFieldsValue({ | 118 | setFieldsValue({ |
| 96 | [FormFieldEnum.ACCESS_MODE]: value ? (options || {}).accessMode : null, | 119 | [FormFieldEnum.ACCESS_MODE]: value ? (options || {}).accessMode : null, |
| 97 | [FormFieldEnum.VIDEO_PLAY_URL]: value ? (options || {}).videoUrl : null, | 120 | [FormFieldEnum.VIDEO_PLAY_URL]: value ? (options || {}).videoUrl : null, |
| @@ -2,14 +2,17 @@ | @@ -2,14 +2,17 @@ | ||
| 2 | import { ComponentMode, ComponentPropsConfigType } from '/@/views/visual/packages/index.type'; | 2 | import { ComponentMode, ComponentPropsConfigType } from '/@/views/visual/packages/index.type'; |
| 3 | import { option } from './config'; | 3 | import { option } from './config'; |
| 4 | import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; | 4 | import { BasicVideoPlay, getVideoTypeByUrl } from '/@/components/Video'; |
| 5 | - import { computed } from 'vue'; | 5 | + import { computed, onMounted, onUnmounted, toRaw } from 'vue'; |
| 6 | import { VideoJsPlayerOptions } from 'video.js'; | 6 | import { VideoJsPlayerOptions } from 'video.js'; |
| 7 | import { AccessMode } from '/@/views/camera/manage/config.data'; | 7 | import { AccessMode } from '/@/views/camera/manage/config.data'; |
| 8 | - import { onMounted } from 'vue'; | ||
| 9 | import { unref } from 'vue'; | 8 | import { unref } from 'vue'; |
| 10 | - import { getStreamingPlayUrl } from '/@/api/camera/cameraManager'; | 9 | + import { closeFlvPlay, getFlvPlayUrl, getStreamingPlayUrl } from '/@/api/camera/cameraManager'; |
| 11 | import { ref } from 'vue'; | 10 | import { ref } from 'vue'; |
| 12 | import { Spin } from 'ant-design-vue'; | 11 | import { Spin } from 'ant-design-vue'; |
| 12 | + import { useFingerprint } from '/@/utils/useFingerprint'; | ||
| 13 | + import { isRtspProtocol } from '/@/components/Video/src/utils'; | ||
| 14 | + import { isShareMode } from '/@/views/sys/share/hook'; | ||
| 15 | + import { getJwtToken, getShareJwtToken } from '/@/utils/auth'; | ||
| 13 | 16 | ||
| 14 | const props = defineProps<{ | 17 | const props = defineProps<{ |
| 15 | config: ComponentPropsConfigType<typeof option>; | 18 | config: ComponentPropsConfigType<typeof option>; |
| @@ -17,15 +20,9 @@ | @@ -17,15 +20,9 @@ | ||
| 17 | 20 | ||
| 18 | const loading = ref(true); | 21 | const loading = ref(true); |
| 19 | 22 | ||
| 20 | - const getIsSelectPreviewMode = computed(() => { | ||
| 21 | - return props.config.option.mode === ComponentMode.SELECT_PREVIEW; | ||
| 22 | - }); | 23 | + const basicVideoPlayEl = ref<Nullable<InstanceType<typeof BasicVideoPlay>>>(null); |
| 23 | 24 | ||
| 24 | - const getIsStreamingMode = computed(() => { | ||
| 25 | - const { option } = props.config; | ||
| 26 | - const { videoConfig } = option; | ||
| 27 | - return videoConfig?.accessMode === AccessMode.Streaming; | ||
| 28 | - }); | 25 | + const withToken = ref(false); |
| 29 | 26 | ||
| 30 | const playSource = ref<Record<'src' | 'type', string>>( | 27 | const playSource = ref<Record<'src' | 'type', string>>( |
| 31 | {} as unknown as Record<'src' | 'type', string> | 28 | {} as unknown as Record<'src' | 'type', string> |
| @@ -34,55 +31,79 @@ | @@ -34,55 +31,79 @@ | ||
| 34 | const exampleVideoPlay = | 31 | const exampleVideoPlay = |
| 35 | 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm'; | 32 | 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm'; |
| 36 | 33 | ||
| 37 | - const getVideoPlaySources = computed(() => { | ||
| 38 | - const { option } = props.config; | ||
| 39 | - const { videoConfig } = option; | ||
| 40 | - const { url } = videoConfig || {}; | ||
| 41 | - return Object.assign( | ||
| 42 | - { | ||
| 43 | - src: unref(getIsSelectPreviewMode) ? exampleVideoPlay : url, | ||
| 44 | - type: getVideoTypeByUrl(url || ''), | ||
| 45 | - }, | ||
| 46 | - unref(playSource) | ||
| 47 | - ); | ||
| 48 | - }); | ||
| 49 | - | ||
| 50 | const getOptions = computed<VideoJsPlayerOptions>(() => { | 34 | const getOptions = computed<VideoJsPlayerOptions>(() => { |
| 51 | return { | 35 | return { |
| 52 | width: '100%' as unknown as number, | 36 | width: '100%' as unknown as number, |
| 53 | height: '100%' as unknown as number, | 37 | height: '100%' as unknown as number, |
| 54 | - sources: unref(getVideoPlaySources).src | ||
| 55 | - ? ([unref(getVideoPlaySources)] as VideoJsPlayerOptions['sources']) | ||
| 56 | - : [], | ||
| 57 | }; | 38 | }; |
| 58 | }); | 39 | }); |
| 59 | 40 | ||
| 41 | + const { getResult } = useFingerprint(); | ||
| 42 | + | ||
| 60 | const handleGetVideoPlayUrl = async () => { | 43 | const handleGetVideoPlayUrl = async () => { |
| 61 | try { | 44 | try { |
| 62 | - if (unref(getIsStreamingMode) && !unref(getVideoPlaySources).src) { | ||
| 63 | - const { option } = props.config; | ||
| 64 | - const { videoConfig } = option; | ||
| 65 | - if (!videoConfig?.id) return; | ||
| 66 | - const { data: { url } = { url: '' } } = await getStreamingPlayUrl(videoConfig?.id); | ||
| 67 | - playSource.value = { | ||
| 68 | - src: url, | ||
| 69 | - type: getVideoTypeByUrl(url), | 45 | + const instance = unref(basicVideoPlayEl)?.getInstance(); |
| 46 | + const { config } = props; | ||
| 47 | + const { option } = config; | ||
| 48 | + const { videoConfig, uuid } = option || {}; | ||
| 49 | + if (!uuid) return; | ||
| 50 | + const { url, id, accessMode } = videoConfig || {}; | ||
| 51 | + const 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 | + } | ||
| 57 | + | ||
| 58 | + if (isRtspProtocol(url!)) { | ||
| 59 | + const result = await getResult(); | ||
| 60 | + const { visitorId } = result; | ||
| 61 | + playUrl = getFlvPlayUrl(playUrl!, visitorId); | ||
| 62 | + withToken.value = true; | ||
| 63 | + | ||
| 64 | + (instance!.options_ as any).flvjs.config.headers = { | ||
| 65 | + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`, | ||
| 70 | }; | 66 | }; |
| 71 | } | 67 | } |
| 68 | + | ||
| 69 | + playSource.value = { | ||
| 70 | + src: playUrl!, | ||
| 71 | + type, | ||
| 72 | + }; | ||
| 73 | + | ||
| 74 | + instance?.src(toRaw(unref(playSource))); | ||
| 75 | + instance?.play(); | ||
| 72 | } finally { | 76 | } finally { |
| 73 | loading.value = false; | 77 | loading.value = false; |
| 74 | } | 78 | } |
| 75 | }; | 79 | }; |
| 76 | 80 | ||
| 81 | + const handleSelectPreview = () => { | ||
| 82 | + loading.value = false; | ||
| 83 | + const instance = unref(basicVideoPlayEl)?.getInstance(); | ||
| 84 | + instance?.src({ type: getVideoTypeByUrl(exampleVideoPlay), src: exampleVideoPlay }); | ||
| 85 | + instance?.play(); | ||
| 86 | + }; | ||
| 87 | + | ||
| 77 | onMounted(() => { | 88 | onMounted(() => { |
| 78 | - handleGetVideoPlayUrl(); | 89 | + const { option } = props.config; |
| 90 | + const { mode } = option; | ||
| 91 | + mode === ComponentMode.SELECT_PREVIEW ? handleSelectPreview() : handleGetVideoPlayUrl(); | ||
| 92 | + }); | ||
| 93 | + | ||
| 94 | + onUnmounted(async () => { | ||
| 95 | + const { visitorId } = await getResult(); | ||
| 96 | + const { option } = props.config; | ||
| 97 | + const { videoConfig } = option || {}; | ||
| 98 | + const { url } = videoConfig || {}; | ||
| 99 | + isRtspProtocol(url!) && closeFlvPlay(url!, visitorId); | ||
| 79 | }); | 100 | }); |
| 80 | </script> | 101 | </script> |
| 81 | 102 | ||
| 82 | <template> | 103 | <template> |
| 83 | <main class="w-full h-full flex flex-col justify-center items-center p-2"> | 104 | <main class="w-full h-full flex flex-col justify-center items-center p-2"> |
| 84 | <Spin :spinning="loading" wrapper-class-name="video-spin"> | 105 | <Spin :spinning="loading" wrapper-class-name="video-spin"> |
| 85 | - <BasicVideoPlay :options="getOptions" /> | 106 | + <BasicVideoPlay ref="basicVideoPlayEl" :options="getOptions" :with-token="withToken" /> |
| 86 | </Spin> | 107 | </Spin> |
| 87 | </main> | 108 | </main> |
| 88 | </template> | 109 | </template> |
| @@ -89,6 +89,7 @@ export interface PublicPresetOptions { | @@ -89,6 +89,7 @@ export interface PublicPresetOptions { | ||
| 89 | maxScale?: number; | 89 | maxScale?: number; |
| 90 | minScale?: number; | 90 | minScale?: number; |
| 91 | componetDesign?: boolean; | 91 | componetDesign?: boolean; |
| 92 | + trendQuery?: boolean; | ||
| 92 | multipleDataSourceComponent?: boolean; | 93 | multipleDataSourceComponent?: boolean; |
| 93 | [ComponentConfigFieldEnum.FONT_COLOR]?: string; | 94 | [ComponentConfigFieldEnum.FONT_COLOR]?: string; |
| 94 | [ComponentConfigFieldEnum.UNIT]?: string; | 95 | [ComponentConfigFieldEnum.UNIT]?: string; |
| @@ -132,7 +133,6 @@ export interface ComponentPropsConfigType<P = PublicPresetOptions, O = DataSourc | @@ -132,7 +133,6 @@ export interface ComponentPropsConfigType<P = PublicPresetOptions, O = DataSourc | ||
| 132 | 133 | ||
| 133 | export interface PackagesType { | 134 | export interface PackagesType { |
| 134 | [PackagesCategoryEnum.TEXT]: ConfigType[]; | 135 | [PackagesCategoryEnum.TEXT]: ConfigType[]; |
| 135 | - [PackagesCategoryEnum.PICTURE]: ConfigType[]; | ||
| 136 | [PackagesCategoryEnum.INSTRUMENT]: ConfigType[]; | 136 | [PackagesCategoryEnum.INSTRUMENT]: ConfigType[]; |
| 137 | [PackagesCategoryEnum.CONTROL]: ConfigType[]; | 137 | [PackagesCategoryEnum.CONTROL]: ConfigType[]; |
| 138 | [PackagesCategoryEnum.MAP]: ConfigType[]; | 138 | [PackagesCategoryEnum.MAP]: ConfigType[]; |
| @@ -17,6 +17,7 @@ | @@ -17,6 +17,7 @@ | ||
| 17 | import { Layout } from 'vue3-grid-layout'; | 17 | import { Layout } from 'vue3-grid-layout'; |
| 18 | import { DataComponentRecord } from '/@/api/dataBoard/model'; | 18 | import { DataComponentRecord } from '/@/api/dataBoard/model'; |
| 19 | import { computed } from 'vue'; | 19 | import { computed } from 'vue'; |
| 20 | + import { useGetComponentConfig } from '../../../packages/hook/useGetComponetConfig'; | ||
| 20 | 21 | ||
| 21 | const props = defineProps<{ | 22 | const props = defineProps<{ |
| 22 | sourceInfo: WidgetDataType; | 23 | sourceInfo: WidgetDataType; |
| @@ -79,6 +80,12 @@ | @@ -79,6 +80,12 @@ | ||
| 79 | return { id, w, h, x, y } as ComponentLayoutType; | 80 | return { id, w, h, x, y } as ComponentLayoutType; |
| 80 | }); | 81 | }); |
| 81 | 82 | ||
| 83 | + const hasTrendQueryIcon = computed(() => { | ||
| 84 | + const frontId = props.sourceInfo.frontId; | ||
| 85 | + const config = useGetComponentConfig(frontId); | ||
| 86 | + return !!config.persetOption?.trendQuery; | ||
| 87 | + }); | ||
| 88 | + | ||
| 82 | async function handleCopy() { | 89 | async function handleCopy() { |
| 83 | const id = props.sourceInfo.id; | 90 | const id = props.sourceInfo.id; |
| 84 | const copyRecord = props.rawDataSource.componentData.find((item) => item.id === id); | 91 | const copyRecord = props.rawDataSource.componentData.find((item) => item.id === id); |
| @@ -123,7 +130,7 @@ | @@ -123,7 +130,7 @@ | ||
| 123 | </Tooltip> | 130 | </Tooltip> |
| 124 | 131 | ||
| 125 | <div v-if="!getIsSharePage" class="flex items-center w-16 justify-evenly"> | 132 | <div v-if="!getIsSharePage" class="flex items-center w-16 justify-evenly"> |
| 126 | - <Tooltip v-if="!isCustomerUser" title="趋势"> | 133 | + <Tooltip v-if="!isCustomerUser && hasTrendQueryIcon" title="趋势"> |
| 127 | <AreaChartOutlined class="text-lg" @click="handleOpenTrendModal" /> | 134 | <AreaChartOutlined class="text-lg" @click="handleOpenTrendModal" /> |
| 128 | </Tooltip> | 135 | </Tooltip> |
| 129 | <AuthDropDown | 136 | <AuthDropDown |