Commit c227c237cf42ea6d20ab7fb7bfb008b779e7cb66

Authored by fengtao
Committed by xp.Huang
1 parent 4ef44921

perf(src/packages/external/Informations): 修改多个摄像头,也支持gbt28181视频播放

src/packages/components/external/Informations/Mores/Camera/components/VideoPlay.vue renamed from src/packages/components/external/Informations/Mores/Camera/components/CameraItem.vue
1 -<template>  
2 - <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }">  
3 - <video crossOrigin="anonymous" :id="`my-player${index}`" ref="videoRef"  
4 - class="video-js my-video vjs-theme-city vjs-big-play-centered">  
5 - </video>  
6 - </div>  
7 -</template>  
8 -<script setup lang="ts">  
9 -import { onMounted, ref, onUnmounted, watch, unref } from 'vue'  
10 -import videojs from 'video.js'  
11 -import 'videojs-flvjs-es6'  
12 -import type { VideoJsPlayerOptions } from 'video.js'  
13 -import 'video.js/dist/video-js.min.css'  
14 -import { getJwtToken, getShareJwtToken } from '@/utils/external/auth'  
15 -import { isShareMode } from '@/views/share/hook'  
16 -import { getOpenFlvPlayUrl, closeFlvPlay } from '@/api/external/flvPlay'  
17 -import { useFingerprint } from '@/utils/external/useFingerprint'  
18 -import { GetResult } from '@fingerprintjs/fingerprintjs'  
19 -  
20 -const props = defineProps({  
21 - sourceSrc: {  
22 - type: String  
23 - },  
24 - name: {  
25 - type: String  
26 - },  
27 - avatar: {  
28 - type: String  
29 - },  
30 - w: {  
31 - type: Number,  
32 - default: 300  
33 - },  
34 - h: {  
35 - type: Number,  
36 - default: 300  
37 - },  
38 - index: {  
39 - type: Number  
40 - }  
41 -})  
42 -  
43 -enum VideoPlayerType {  
44 - m3u8 = 'application/x-mpegURL',  
45 - mp4 = 'video/mp4',  
46 - webm = 'video/webm',  
47 - flv = 'video/x-flv',  
48 -}  
49 -  
50 -const isRtspProtocol = (url: string) => {  
51 - const reg = /^rtsp:\/\//g;  
52 - return reg.test(url);  
53 -};  
54 -  
55 -const getVideoTypeByUrl = (url = '') => {  
56 - try {  
57 - const { protocol, pathname } = new URL(url)  
58 -  
59 - if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv  
60 -  
61 - const reg = /[^.]\w*$/  
62 - const mathValue = pathname.match(reg) || []  
63 - const ext = mathValue[0] as keyof typeof VideoPlayerType || 'webm'  
64 - const type = VideoPlayerType[ext]  
65 - return type ? type : VideoPlayerType.webm  
66 - } catch (error) {  
67 - console.error(error)  
68 - return VideoPlayerType.webm  
69 - }  
70 -};  
71 -  
72 -  
73 -// video标签  
74 -const videoRef = ref<HTMLElement | null>(null)  
75 -  
76 -// video实例对象  
77 -let videoPlayer: videojs.Player | null = null  
78 -  
79 -const fingerprintResult = ref<Nullable<GetResult>>(null)  
80 -  
81 -//options配置  
82 -const options: VideoJsPlayerOptions & Recordable = {  
83 - language: 'zh-CN', // 设置语言  
84 - controls: true, // 是否显示控制条  
85 - preload: 'auto', // 预加载  
86 - autoplay: true, // 是否自动播放  
87 - fluid: false, // 自适应宽高  
88 - poster: props?.avatar || '',  
89 - // src: getSource() || '', // 要嵌入的视频源的源 URL  
90 - sources: [],  
91 - muted: true,  
92 - userActions: {  
93 - hotkeys: true  
94 - },  
95 - techOrder: ['html5', 'flvjs'],  
96 - flvjs: {  
97 - mediaDataSource: {  
98 - isLive: true,  
99 - cors: true,  
100 - withCredentials: false,  
101 - hasAudio: false  
102 - },  
103 - config: {  
104 - autoCleanupSourceBuffer: true,  
105 - }  
106 - },  
107 -}  
108 -  
109 -  
110 -const { getResult } = useFingerprint()  
111 -async function getSource() {  
112 - fingerprintResult.value = await getResult()  
113 - let src = props.sourceSrc || ''  
114 -  
115 - if (isRtspProtocol(props.sourceSrc!)) {  
116 - src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '')  
117 - }  
118 -  
119 - return [  
120 - {  
121 - type: getVideoTypeByUrl(props.sourceSrc),  
122 - src  
123 - }  
124 - ]  
125 -}  
126 -  
127 -// 初始化videojs  
128 -const initVideo = async () => {  
129 - if (videoRef.value) {  
130 - // 创建 video 实例  
131 - options.sources = await getSource()  
132 - if (options.sources && options.sources.length) {  
133 -  
134 - if (isRtspProtocol(props.sourceSrc || '')) {  
135 - options.flvjs = {  
136 - ...(options.flvjs || {}),  
137 - config: {  
138 - headers: {  
139 - 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`,  
140 - }  
141 - }  
142 - }  
143 - }  
144 - videoPlayer = videojs(videoRef.value, options)  
145 - }  
146 - }  
147 -}  
148 -  
149 -watch(  
150 - () => props.sourceSrc,  
151 - async (newData: any) => {  
152 - const result = await getSource()  
153 - // props.sourceSrc = newData  
154 - videoPlayer?.src(result) as any  
155 - videoPlayer?.play()  
156 - },  
157 - {  
158 - immediate: true  
159 - }  
160 -)  
161 -  
162 -onMounted(() => {  
163 - initVideo()  
164 -})  
165 -  
166 -onUnmounted(() => {  
167 - if (props.sourceSrc) {  
168 - closeFlvPlay(props.sourceSrc, unref(fingerprintResult)!.visitorId!)  
169 - }  
170 - handleVideoDispose()  
171 -})  
172 -  
173 -//播放  
174 -const handleVideoPlay = () => videoPlayer?.play()  
175 -  
176 -const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()  
177 -//暂停  
178 -defineExpose({  
179 - handleVideoPlay,  
180 - handleVideoDispose  
181 -})  
182 -</script>  
183 -  
184 -<style lang="scss" scoped>  
185 -.go-content-box {  
186 - display: flex;  
187 - align-items: center;  
188 - justify-content: center;  
189 -  
190 - .my-video {  
191 - width: 100% !important;  
192 - height: 100% !important;  
193 - position: relative;  
194 - }  
195 -}  
196 -</style> 1 +<template>
  2 + <div class="go-content-box" :style="{ width: w + 'px', height: h + 'px' }">
  3 + <video
  4 + crossOrigin="anonymous"
  5 + :id="`my-player${index}`"
  6 + ref="videoRef"
  7 + class="video-js my-video vjs-theme-city vjs-big-play-centered"
  8 + ></video>
  9 + </div>
  10 +</template>
  11 +<script setup lang="ts" name="VideoPlay">
  12 +import { onMounted, ref, onUnmounted, watch, unref, nextTick } from 'vue'
  13 +import videojs from 'video.js'
  14 +import 'videojs-flvjs-es6'
  15 +import type { VideoJsPlayerOptions } from 'video.js'
  16 +import 'video.js/dist/video-js.min.css'
  17 +import { getJwtToken, getShareJwtToken } from '@/utils/external/auth'
  18 +import { isShareMode } from '@/views/share/hook'
  19 +import { getOpenFlvPlayUrl, closeFlvPlay } from '@/api/external/flvPlay'
  20 +import { useFingerprint } from '@/utils/external/useFingerprint'
  21 +import { GetResult } from '@fingerprintjs/fingerprintjs'
  22 +import { VideoPlayerType } from '../config'
  23 +
  24 +const props = defineProps({
  25 + sourceSrc: {
  26 + type: String
  27 + },
  28 + autoPlay: {
  29 + type: Boolean
  30 + },
  31 + name: {
  32 + type: String
  33 + },
  34 + avatar: {
  35 + type: String
  36 + },
  37 + w: {
  38 + type: Number,
  39 + default: 300
  40 + },
  41 + h: {
  42 + type: Number,
  43 + default: 300
  44 + },
  45 + index: {
  46 + type: Number
  47 + }
  48 +})
  49 +
  50 +const isRtspProtocol = (url: string) => {
  51 + const reg = /^rtsp:\/\//g
  52 + return reg.test(url)
  53 +}
  54 +
  55 +const getVideoTypeByUrl = (url = '') => {
  56 + if(!url) return;
  57 + try {
  58 + const { protocol, pathname } = new URL(url)
  59 + if (protocol.startsWith('rtsp:')) return VideoPlayerType.flv
  60 + const reg = /[^.]\w*$/
  61 + const mathValue = pathname.match(reg) || []
  62 + const ext = (mathValue[0] as keyof typeof VideoPlayerType) || 'webm'
  63 + const type = VideoPlayerType[ext]
  64 + return type ? type : VideoPlayerType.webm
  65 + } catch (error) {
  66 + console.error(error)
  67 + return VideoPlayerType.webm
  68 + }
  69 +}
  70 +
  71 +// video标签
  72 +const videoRef = ref<HTMLElement | null>(null)
  73 +
  74 +// video实例对象
  75 +let videoPlayer: videojs.Player | null = null
  76 +
  77 +const fingerprintResult = ref<Nullable<GetResult>>(null)
  78 +
  79 +//options配置
  80 +const options: VideoJsPlayerOptions & Recordable = {
  81 + language: 'zh-CN', // 设置语言
  82 + controls: true, // 是否显示控制条
  83 + preload: 'auto', // 预加载
  84 + autoplay: props.autoPlay ? true : false, // 是否自动播放
  85 + fluid: false, // 自适应宽高
  86 + poster: props?.avatar || '',
  87 + sources: [],
  88 + muted: props.autoPlay ? true : false,
  89 + userActions: {
  90 + hotkeys: true
  91 + },
  92 + techOrder: ['html5', 'flvjs'],
  93 + flvjs: {
  94 + mediaDataSource: {
  95 + isLive: true,
  96 + cors: true,
  97 + withCredentials: false,
  98 + hasAudio: false
  99 + },
  100 + config: {
  101 + autoCleanupSourceBuffer: true
  102 + }
  103 + }
  104 +}
  105 +
  106 +const { getResult } = useFingerprint()
  107 +
  108 +async function getSource() {
  109 + fingerprintResult.value = await getResult()
  110 + let src = props.sourceSrc || ''
  111 + if (isRtspProtocol(props.sourceSrc!)) {
  112 + src = getOpenFlvPlayUrl(src, unref(fingerprintResult)?.visitorId || '')
  113 + }
  114 + return [
  115 + {
  116 + type: getVideoTypeByUrl(props.sourceSrc),
  117 + src
  118 + }
  119 + ]
  120 +}
  121 +
  122 +// 初始化videojs
  123 +const initVideo = async () => {
  124 + await nextTick()
  125 + try {
  126 + if (videoRef.value) {
  127 + // 创建 video 实例
  128 + options.sources = await getSource()
  129 + if (options.sources && options.sources.length) {
  130 + if (isRtspProtocol(props.sourceSrc || '')) {
  131 + options.flvjs = {
  132 + ...(options.flvjs || {}),
  133 + config: {
  134 + headers: {
  135 + 'X-Authorization': `Bearer ${isShareMode() ? getShareJwtToken() : getJwtToken()}`
  136 + }
  137 + }
  138 + }
  139 + }
  140 + if (options.sources.at(-1)?.src){
  141 + videoPlayer = videojs(videoRef.value, options)
  142 + //fix 修复videojs解决直播延时的问题
  143 + videoPlayer?.on('timeupdate', function () {
  144 + // 计算表最新推流的时间和现在播放器播放推流的时间
  145 + let differTime =
  146 + (videoPlayer as any as videojs.Player)?.buffered()?.end(0) -
  147 + (videoPlayer as any as videojs.Player)?.currentTime() // 差值小于1.5s时根据1倍速进行播放
  148 + if (differTime < 1.5) {
  149 + videoPlayer?.playbackRate(1)
  150 + } // 差值大于1.5s小于10s根据1.2倍速进行播放
  151 + if (differTime < 10 && differTime > 1.5) {
  152 + videoPlayer?.playbackRate(1.2)
  153 + } // 差值大于10s时进行重新加载直播流
  154 + if (differTime > 10) {
  155 + initVideo()
  156 + }
  157 + }) //
  158 + }
  159 + }
  160 + }
  161 + } catch (e) {
  162 + console.log(e)
  163 + }
  164 +}
  165 +
  166 +watch(
  167 + () => props.sourceSrc,
  168 + async () => {
  169 + (props as any ).sourceSrc = ''
  170 + if(!props.sourceSrc) return;
  171 + await nextTick()
  172 + if(unref(fingerprintResult)!.visitorId!) {
  173 + closeFlvPlay(props.sourceSrc!, unref(fingerprintResult)!.visitorId!)
  174 + }
  175 + videoPlayer?.src('');
  176 + initVideo()
  177 + videoPlayer?.src({
  178 + type: getVideoTypeByUrl(props.sourceSrc),
  179 + src: props.sourceSrc!
  180 + })
  181 + videoPlayer?.load()
  182 + videoPlayer?.play()
  183 + },
  184 + {
  185 + immediate: true
  186 + }
  187 +)
  188 +
  189 +watch(
  190 + () => props.autoPlay,
  191 + async (newData: boolean) => {
  192 + if (newData) {
  193 + handleVideoPlay()
  194 + } else {
  195 + videoPlayer?.pause()
  196 + }
  197 + }
  198 +)
  199 +
  200 +onMounted(() => {
  201 + initVideo()
  202 +})
  203 +
  204 +onUnmounted(() => {
  205 + if (props.sourceSrc) {
  206 + closeFlvPlay(props.sourceSrc, unref(fingerprintResult)!.visitorId!)
  207 + }
  208 + handleVideoDispose()
  209 +})
  210 +
  211 +//播放
  212 +const handleVideoPlay = () => videoPlayer?.play()
  213 +
  214 +//暂停和销毁
  215 +const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
  216 +
  217 +</script>
  218 +
  219 +<style lang="scss" scoped>
  220 +.go-content-box {
  221 + display: flex;
  222 + align-items: center;
  223 + justify-content: center;
  224 +
  225 + .my-video {
  226 + width: 100% !important;
  227 + height: 100% !important;
  228 + position: relative;
  229 + }
  230 +}
  231 +</style>
1 -import CameraItem from './CameraItem.vue' 1 +import VideoPlay from './VideoPlay.vue'
2 2
3 -export { CameraItem } 3 +export { VideoPlay as default }
@@ -3,26 +3,70 @@ import { CreateComponentType } from '@/packages/index.d' @@ -3,26 +3,70 @@ import { CreateComponentType } from '@/packages/index.d'
3 import { CameraConfig } from './index' 3 import { CameraConfig } from './index'
4 import cloneDeep from 'lodash/cloneDeep' 4 import cloneDeep from 'lodash/cloneDeep'
5 5
  6 +export enum VideoPlayerType {
  7 + m3u8 = 'application/x-mpegURL',
  8 + mp4 = 'video/mp4',
  9 + webm = 'video/webm',
  10 + flv = 'video/x-flv',
  11 +}
  12 +
6 export enum sourceTypeEnum { 13 export enum sourceTypeEnum {
7 CUSTOM = 'custom', 14 CUSTOM = 'custom',
8 PLATFORM = 'platform' 15 PLATFORM = 'platform'
9 } 16 }
10 17
  18 +export enum sourceTypeNameEnum {
  19 + CUSTOM = '自定义',
  20 + PLATFORM = '平台获取'
  21 +}
  22 +
  23 +export interface datasetList {
  24 + url: string | null
  25 + sourceType: string
  26 + organization: string
  27 + autoPlay: boolean
  28 + className?: string[] & string,
  29 + sty?: Recordable
  30 +}
  31 +
  32 +export enum AccessModeEnum {
  33 + ManuallyEnter = 0, //手动输入
  34 + Streaming = 1, //流媒体
  35 + GBT28181 = 2 //GBT28181
  36 +}
  37 +
  38 +export interface GBT28181Params {
  39 + channelNo: string
  40 + deviceId: string
  41 + deviceName: string
  42 +}
  43 +
  44 +export interface videoList {
  45 + name: string
  46 + accessMode: number
  47 + id: string
  48 + videoUrl: string
  49 + label: string
  50 + value: string
  51 + sn: string
  52 + channelId: string
  53 + deviceId: string
  54 + customUrl: string
  55 + params: GBT28181Params
  56 +}
  57 +
11 export const option = { 58 export const option = {
12 - autoSwitch: false, 59 + autoSwitch: false, //开启切换
  60 + interval: 2000, // 自动播放的间隔(ms)
13 dataset: [ 61 dataset: [
14 { 62 {
15 - byIdUrl: '', //通过接口获取的url  
16 - url: '', //直接获取视频列表的url  
17 - sourceType: 'custom',  
18 - organization: '' 63 + id: null,
  64 + url: null,
  65 + sourceType: sourceTypeEnum.CUSTOM,
  66 + organization: '',
  67 + autoPlay: true
19 } 68 }
20 - ] as any,  
21 - // 自动播放的间隔(ms)  
22 - interval: 2000,  
23 - autoplay: true,  
24 - effect: 'slide',  
25 - displayMode: 'singleGrid' 69 + ],
26 } 70 }
27 71
28 export default class Config extends PublicConfigClass implements CreateComponentType { 72 export default class Config extends PublicConfigClass implements CreateComponentType {
@@ -11,11 +11,16 @@ @@ -11,11 +11,16 @@
11 </setting-item> 11 </setting-item>
12 </setting-item-box> 12 </setting-item-box>
13 <template v-for="(item, index) in optionData.dataset" :key="index"> 13 <template v-for="(item, index) in optionData.dataset" :key="index">
  14 + <setting-item-box name="自动播放" :alone="true">
  15 + <setting-item>
  16 + <n-switch v-model:value="item.autoPlay" size="small"></n-switch>
  17 + </setting-item>
  18 + </setting-item-box>
14 <setting-item-box name="源类型" :alone="true"> 19 <setting-item-box name="源类型" :alone="true">
15 <setting-item> 20 <setting-item>
16 <n-radio-group @change="handleChecked($event, item, index)" v-model:value="item.sourceType" name="radiogroup"> 21 <n-radio-group @change="handleChecked($event, item, index)" v-model:value="item.sourceType" name="radiogroup">
17 <n-space> 22 <n-space>
18 - <n-radio v-for="(item, index) in sourceTypes" :key="item.value" :value="item.value"> 23 + <n-radio v-for="(item, index) in sourceTypes" :key="item.value + index" :value="item.value">
19 {{ item.label }} 24 {{ item.label }}
20 </n-radio> 25 </n-radio>
21 </n-space> 26 </n-space>
@@ -45,8 +50,8 @@ @@ -45,8 +50,8 @@
45 <setting-item-box v-if="item.sourceType === sourceTypeEnum.PLATFORM" name="视频" :alone="true"> 50 <setting-item-box v-if="item.sourceType === sourceTypeEnum.PLATFORM" name="视频" :alone="true">
46 <setting-item> 51 <setting-item>
47 <n-select 52 <n-select
48 - v-model:value="item.url"  
49 - @update:value="(value:string,e:any) => handleSelect(value,e, index)" 53 + v-model:value="item.id"
  54 + @update:value="(value: string, e: videoList) => handleSelect(value, e, index)"
50 :options="videoOptions" 55 :options="videoOptions"
51 /> 56 />
52 </setting-item> 57 </setting-item>
@@ -57,7 +62,15 @@ @@ -57,7 +62,15 @@
57 style="margin-left: 10px" 62 style="margin-left: 10px"
58 v-if="optionData.dataset.length < 9" 63 v-if="optionData.dataset.length < 9"
59 size="small" 64 size="small"
60 - @click="optionData.dataset.push({ url: '', sourceType: 'custom', organization: '', byIdUrl: '' })" 65 + @click="
  66 + optionData.dataset.push({
  67 + url: null,
  68 + id: null,
  69 + sourceType: sourceTypeEnum.CUSTOM,
  70 + organization: '',
  71 + autoPlay: true
  72 + })
  73 + "
61 > 74 >
62 + 75 +
63 </n-button> 76 </n-button>
@@ -66,10 +79,11 @@ @@ -66,10 +79,11 @@
66 79
67 <script setup lang="ts"> 80 <script setup lang="ts">
68 import { PropType, ref, onMounted } from 'vue' 81 import { PropType, ref, onMounted } from 'vue'
69 -import { option, sourceTypeEnum } from './config' 82 +import { AccessModeEnum, datasetList, option, sourceTypeEnum, sourceTypeNameEnum, videoList } from './config'
70 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting' 83 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
71 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index' 84 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index'
72 import { NTreeSelect } from 'naive-ui' 85 import { NTreeSelect } from 'naive-ui'
  86 +import { getVideoControlStart } from '@/api/external/flvPlay'
73 87
74 const props = defineProps({ 88 const props = defineProps({
75 optionData: { 89 optionData: {
@@ -80,66 +94,99 @@ const props = defineProps({ @@ -80,66 +94,99 @@ const props = defineProps({
80 94
81 const sourceTypes = [ 95 const sourceTypes = [
82 { 96 {
83 - value: 'custom',  
84 - label: '自定义' 97 + value: sourceTypeEnum.CUSTOM,
  98 + label: sourceTypeNameEnum.CUSTOM
85 }, 99 },
86 { 100 {
87 - value: 'platform',  
88 - label: '平台获取' 101 + value: sourceTypeEnum.PLATFORM,
  102 + label: sourceTypeNameEnum.PLATFORM
89 } 103 }
90 ] 104 ]
91 105
92 -const originationOption = ref([]) 106 +const originationOption = ref<Recordable[]>([])
93 107
94 -const videoOptions = ref([]) 108 +const videoOptions = ref<videoList[]>([])
95 109
96 const getOriginationList = async () => { 110 const getOriginationList = async () => {
97 const res = await getOrganizationList() 111 const res = await getOrganizationList()
98 originationOption.value = res 112 originationOption.value = res
99 } 113 }
100 114
101 -const handleUpdateTreeValue = (e: any) => { 115 +const handleUpdateTreeValue = (e: string) => {
102 getVideoLists(e) 116 getVideoLists(e)
103 } 117 }
104 118
105 const getVideoLists = async (organizationId: string) => { 119 const getVideoLists = async (organizationId: string) => {
106 const res = await getVideoList({ organizationId }) 120 const res = await getVideoList({ organizationId })
107 - videoOptions.value = res?.data?.map((item: any) => ({ 121 + if (!res) return
  122 + videoOptions.value = res?.data?.map((item: videoList) => ({
108 label: item.name, 123 label: item.name,
109 - value: item.accessMode === 1 ? item.id : item.videoUrl, 124 + value: item.id,
110 id: item.id, 125 id: item.id,
111 - accessMode: item.accessMode 126 + accessMode: item.accessMode,
  127 + customUrl: item.accessMode === AccessModeEnum.ManuallyEnter ? item.videoUrl: '', //参数只给自定义视频流使用
  128 + channelId: item.accessMode === AccessModeEnum.GBT28181 ? item?.params?.channelNo : '', //参数只给gbt28181使用
  129 + deviceId: item.accessMode === AccessModeEnum.GBT28181 ? item?.params?.deviceId : '' //参数只给gbt28181使用
112 })) 130 }))
113 } 131 }
114 132
115 -const handleChecked = ({ target }: any, values: object, index: number) => {  
116 - const { value } = target  
117 - if (value === sourceTypeEnum.PLATFORM) {  
118 - getOriginationList()  
119 - }  
120 - props.optionData.dataset[index].url = ''  
121 -}  
122 -  
123 -const getVideoUrlById = async (e: any, id: string, index: number) => { 133 +//针对萤石云或者海康威视,根据视频id获取播放流地址
  134 +const getVideoUrlById = async (_: videoList, id: string, index: number) => {
124 const res = await getVideoUrl(id) 135 const res = await getVideoUrl(id)
125 if (!res) return 136 if (!res) return
126 const { url } = res.data 137 const { url } = res.data
127 - props.optionData.dataset.forEach((item: any, itemIndex: number) => { 138 + props.optionData.dataset.forEach((item: datasetList, itemIndex: number) => {
128 if (itemIndex === index) { 139 if (itemIndex === index) {
129 - item.byIdUrl = url 140 + item.url = url
130 } 141 }
131 }) 142 })
132 } 143 }
133 144
134 -const handleSelect = (value: string, e: any, index: number) => {  
135 - const { accessMode, id } = e  
136 - if (accessMode === 1) { 145 +//针对gbt28181,根据设备id和通道号获取播放流地址
  146 +const getVideoControlList = async (deviceId: string, channelId: string, index: number) => {
  147 + const {
  148 + data: { flv }
  149 + } = await getVideoControlStart({
  150 + deviceId,
  151 + channelId
  152 + })
  153 + props.optionData.dataset.forEach((item: datasetList, itemIndex: number) => {
  154 + if (itemIndex === index) {
  155 + item.url = flv
  156 + }
  157 + })
  158 +}
  159 +
  160 +//针对自定义地址,直接获取地址
  161 +const getCustomUrl= async (url:string, index: number) => {
  162 + props.optionData.dataset.forEach((item: datasetList, itemIndex: number) => {
  163 + if (itemIndex === index) {
  164 + item.url = url
  165 + }
  166 + })
  167 +}
  168 +
  169 +const handleChecked = ({ target }: Recordable, _: object, index: number) => {
  170 + const { value } = target
  171 + if (value === sourceTypeEnum.PLATFORM) {
  172 + getOriginationList()
  173 + }
  174 + props.optionData.dataset[index].url = null
  175 +}
  176 +
  177 +const handleSelect = (_: string, e: videoList, index: number) => {
  178 + const { accessMode, id, channelId, deviceId, customUrl } = e
  179 + if (accessMode === AccessModeEnum.Streaming) {
137 getVideoUrlById(e, id, index) 180 getVideoUrlById(e, id, index)
  181 + } else if (accessMode === AccessModeEnum.GBT28181) {
  182 + getVideoControlList(deviceId, channelId, index)
  183 + } else {
  184 + getCustomUrl(customUrl, index)
138 } 185 }
139 } 186 }
140 187
141 onMounted(() => { 188 onMounted(() => {
142 - props.optionData.dataset.forEach((item: any) => { 189 + props.optionData.dataset.forEach((item: datasetList) => {
143 if (item.sourceType === sourceTypeEnum.PLATFORM) { 190 if (item.sourceType === sourceTypeEnum.PLATFORM) {
144 getOriginationList() 191 getOriginationList()
145 if (item.organization) { 192 if (item.organization) {
1 <template> 1 <template>
2 <div @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" class="banner-box" ref="root"> 2 <div @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" class="banner-box" ref="root">
3 <div class="wrapper"> 3 <div class="wrapper">
4 - <div v-for="(item, index) in option.dataset" :key="index + item" :class="item.className" :style="item.sty">  
5 - <CameraItem  
6 - ref="cameraRef" 4 + <div v-for="(item, index) in option.dataset" :key="index" :class="item.className" :style="item.sty">
  5 + <VideoPlay
  6 + :autoPlay="item.autoPlay"
7 :name="item.name" 7 :name="item.name"
8 :avatar="item.avatar" 8 :avatar="item.avatar"
9 :key="item + index" 9 :key="item + index"
10 - :sourceSrc="!item.byIdUrl ? item.url : item.byIdUrl" 10 + :sourceSrc="item.url"
11 :w="w" 11 :w="w"
12 :h="h" 12 :h="h"
13 :index="index" 13 :index="index"
@@ -23,8 +23,8 @@ @@ -23,8 +23,8 @@
23 import { PropType, watch, toRefs, shallowReactive, onMounted, ref } from 'vue' 23 import { PropType, watch, toRefs, shallowReactive, onMounted, ref } from 'vue'
24 import { CreateComponentType } from '@/packages/index.d' 24 import { CreateComponentType } from '@/packages/index.d'
25 import 'video.js/dist/video-js.min.css' 25 import 'video.js/dist/video-js.min.css'
26 -import { option as configOption } from './config'  
27 -import { CameraItem } from './components' 26 +import { option as typeOption } from './config'
  27 +import VideoPlay from './components'
28 28
29 const props = defineProps({ 29 const props = defineProps({
30 chartConfig: { 30 chartConfig: {
@@ -39,15 +39,14 @@ const { w, h } = toRefs(props.chartConfig.attr) @@ -39,15 +39,14 @@ const { w, h } = toRefs(props.chartConfig.attr)
39 39
40 const { autoSwitch, interval } = toRefs(props.chartConfig.option) 40 const { autoSwitch, interval } = toRefs(props.chartConfig.option)
41 41
42 -const option = shallowReactive({  
43 - dataset: configOption.dataset 42 +//暂定 any类型
  43 +const option = shallowReactive<{ ['dataset']: any }>({
  44 + dataset: typeOption.dataset
44 }) 45 })
45 46
46 -const cameraRef = ref<InstanceType<typeof CameraItem>>()  
47 -  
48 let initial = ref(0) 47 let initial = ref(0)
49 48
50 -const computedFunc = (initial: number, source: any) => { 49 +const computedFunc = (initial: number, source: Recordable[]) => {
51 if (initial < 0) initial = 0 50 if (initial < 0) initial = 0
52 if (Array.isArray(source)) { 51 if (Array.isArray(source)) {
53 let len = source.length, 52 let len = source.length,
@@ -56,14 +55,14 @@ const computedFunc = (initial: number, source: any) => { @@ -56,14 +55,14 @@ const computedFunc = (initial: number, source: any) => {
56 temp3 = initial, 55 temp3 = initial,
57 temp4 = initial + 1 >= len ? initial + 1 - len : initial + 1, 56 temp4 = initial + 1 >= len ? initial + 1 - len : initial + 1,
58 temp5 = initial + 2 >= len ? initial + 2 - len : initial + 2 57 temp5 = initial + 2 >= len ? initial + 2 - len : initial + 2
59 - return source?.map((item: any, index: number) => { 58 + return source?.map((item: Recordable, index: number) => {
60 let transform = `translateX(-50%) scale(0.7)`, 59 let transform = `translateX(-50%) scale(0.7)`,
61 zIndex = 0, 60 zIndex = 0,
62 className = 'slide' 61 className = 'slide'
63 switch (index) { 62 switch (index) {
64 case temp3: 63 case temp3:
65 transform = `translateX(-50%) scale(1)` 64 transform = `translateX(-50%) scale(1)`
66 - className = ['slide', 'activate'] as any 65 + className = ['slide', 'activate'] as string[] & string
67 zIndex = 300 66 zIndex = 300
68 break 67 break
69 case temp1: 68 case temp1:
@@ -114,6 +113,7 @@ watch( @@ -114,6 +113,7 @@ watch(
114 ) 113 )
115 114
116 // 处理自动轮播 115 // 处理自动轮播
  116 +// 暂定 any类型
117 let timer: any = null 117 let timer: any = null
118 118
119 const autoPlay = () => { 119 const autoPlay = () => {
@@ -126,15 +126,14 @@ const autoPlay = () => { @@ -126,15 +126,14 @@ const autoPlay = () => {
126 } 126 }
127 127
128 // 鼠标移入移除效果 128 // 鼠标移入移除效果
129 -let root = ref(null) 129 +let root = ref<HTMLElement>()
130 130
131 onMounted(() => { 131 onMounted(() => {
132 clearInterval(timer) 132 clearInterval(timer)
133 - const box: any = root.value  
134 - box.onmouseenter = () => clearInterval(timer) 133 + root.value!.onmouseenter = () => clearInterval(timer)
135 if (autoSwitch.value) { 134 if (autoSwitch.value) {
136 autoPlay() 135 autoPlay()
137 - box.onmouseleave = () => autoPlay() 136 + root.value!.onmouseleave = () => autoPlay()
138 } 137 }
139 }) 138 })
140 139
@@ -155,6 +154,20 @@ function changeSlide(dir: string) { @@ -155,6 +154,20 @@ function changeSlide(dir: string) {
155 changeVideo(dir) 154 changeVideo(dir)
156 } 155 }
157 156
  157 +watch(
  158 + () => autoSwitch.value,
  159 + (newV: boolean) => {
  160 + if (newV) {
  161 + autoPlay()
  162 + } else {
  163 + clearInterval(timer)
  164 + }
  165 + },
  166 + {
  167 + immediate: true
  168 + }
  169 +)
  170 +
158 const handleMouseenter = () => { 171 const handleMouseenter = () => {
159 isShowSvg.value = true 172 isShowSvg.value = true
160 } 173 }
@@ -9,7 +9,7 @@ @@ -9,7 +9,7 @@
9 </div> 9 </div>
10 </template> 10 </template>
11 <script setup lang="ts"> 11 <script setup lang="ts">
12 -import { onMounted, ref, onUnmounted, watch, unref, PropType } from 'vue' 12 +import { onMounted, ref, onUnmounted, watch, unref, PropType, nextTick } from 'vue'
13 import videojs from 'video.js' 13 import videojs from 'video.js'
14 import 'videojs-flvjs-es6' 14 import 'videojs-flvjs-es6'
15 import type { VideoJsPlayerOptions } from 'video.js' 15 import type { VideoJsPlayerOptions } from 'video.js'
@@ -45,6 +45,7 @@ const isRtspProtocol = (url: string) => { @@ -45,6 +45,7 @@ const isRtspProtocol = (url: string) => {
45 } 45 }
46 46
47 const getVideoTypeByUrl = (url = '') => { 47 const getVideoTypeByUrl = (url = '') => {
  48 + if(!url) return;
48 try { 49 try {
49 const { protocol, pathname } = new URL(url) 50 const { protocol, pathname } = new URL(url)
50 if (protocol.startsWith('rtsp:')) return VideoPlayerTypeEnum.flv 51 if (protocol.startsWith('rtsp:')) return VideoPlayerTypeEnum.flv
@@ -112,6 +113,7 @@ async function getSource() { @@ -112,6 +113,7 @@ async function getSource() {
112 113
113 // 初始化videojs 114 // 初始化videojs
114 const initVideo = async () => { 115 const initVideo = async () => {
  116 + await nextTick()
115 if (videoRef.value) { 117 if (videoRef.value) {
116 // 创建 video 实例 118 // 创建 video 实例
117 options.sources = await getSource() 119 options.sources = await getSource()
@@ -126,22 +128,24 @@ const initVideo = async () => { @@ -126,22 +128,24 @@ const initVideo = async () => {
126 } 128 }
127 } 129 }
128 } 130 }
129 - videoPlayer = videojs(videoRef.value, options) //fix 修复videojs解决直播延时的问题  
130 - videoPlayer?.on('timeupdate', function () {  
131 - // 计算表最新推流的时间和现在播放器播放推流的时间  
132 - let differTime =  
133 - (videoPlayer as any as videojs.Player)?.buffered()?.end(0) -  
134 - (videoPlayer as any as videojs.Player)?.currentTime() // 差值小于1.5s时根据1倍速进行播放  
135 - if (differTime < 1.5) {  
136 - videoPlayer?.playbackRate(1)  
137 - } // 差值大于1.5s小于10s根据1.2倍速进行播放  
138 - if (differTime < 10 && differTime > 1.5) {  
139 - videoPlayer?.playbackRate(1.2)  
140 - } // 差值大于10s时进行重新加载直播流  
141 - if (differTime > 10) {  
142 - initVideo()  
143 - }  
144 - }) // 131 + if (options.sources.at(-1)?.src){
  132 + videoPlayer = videojs(videoRef.value, options) //fix 修复videojs解决直播延时的问题
  133 + videoPlayer?.on('timeupdate', function () {
  134 + // 计算表最新推流的时间和现在播放器播放推流的时间
  135 + let differTime =
  136 + (videoPlayer as any as videojs.Player)?.buffered()?.end(0) -
  137 + (videoPlayer as any as videojs.Player)?.currentTime() // 差值小于1.5s时根据1倍速进行播放
  138 + if (differTime < 1.5) {
  139 + videoPlayer?.playbackRate(1)
  140 + } // 差值大于1.5s小于10s根据1.2倍速进行播放
  141 + if (differTime < 10 && differTime > 1.5) {
  142 + videoPlayer?.playbackRate(1.2)
  143 + } // 差值大于10s时进行重新加载直播流
  144 + if (differTime > 10) {
  145 + initVideo()
  146 + }
  147 + }) //
  148 + }
145 } 149 }
146 } 150 }
147 } 151 }
@@ -149,12 +153,20 @@ const initVideo = async () => { @@ -149,12 +153,20 @@ const initVideo = async () => {
149 watch( 153 watch(
150 () => props.sourceSrc, 154 () => props.sourceSrc,
151 async () => { 155 async () => {
  156 + (props as any ).sourceSrc = ''
  157 + if(!props.sourceSrc) return;
  158 + await nextTick();
  159 + if(unref(fingerprintResult)!.visitorId!) {
  160 + closeFlvPlay(props.sourceSrc!, unref(fingerprintResult)!.visitorId!)
  161 + }
  162 + videoPlayer?.src('');
  163 + initVideo()
152 videoPlayer?.src({ 164 videoPlayer?.src({
153 type: getVideoTypeByUrl(props.sourceSrc), 165 type: getVideoTypeByUrl(props.sourceSrc),
154 src: props.sourceSrc! 166 src: props.sourceSrc!
155 }); 167 });
156 videoPlayer?.load(); 168 videoPlayer?.load();
157 - videoPlayer?.play() 169 + videoPlayer?.play();
158 }, 170 },
159 { 171 {
160 immediate: true 172 immediate: true
@@ -170,9 +182,6 @@ watch( @@ -170,9 +182,6 @@ watch(
170 videoPlayer?.pause() 182 videoPlayer?.pause()
171 } 183 }
172 }, 184 },
173 - {  
174 - immediate: true  
175 - }  
176 ) 185 )
177 186
178 onMounted(() => { 187 onMounted(() => {
@@ -191,10 +200,7 @@ const handleVideoPlay = () => videoPlayer?.play() @@ -191,10 +200,7 @@ const handleVideoPlay = () => videoPlayer?.play()
191 200
192 //暂停和销毁 201 //暂停和销毁
193 const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause() 202 const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
194 -defineExpose({  
195 - handleVideoPlay,  
196 - handleVideoDispose  
197 -}) 203 +
198 </script> 204 </script>
199 205
200 <style lang="scss" scoped> 206 <style lang="scss" scoped>
@@ -8,6 +8,11 @@ export enum sourceTypeEnum { @@ -8,6 +8,11 @@ export enum sourceTypeEnum {
8 PLATFORM = 'platform' 8 PLATFORM = 'platform'
9 } 9 }
10 10
  11 +export enum sourceTypeNameEnum {
  12 + CUSTOM = '自定义',
  13 + PLATFORM = '平台获取'
  14 +}
  15 +
11 export enum VideoPlayerTypeEnum { 16 export enum VideoPlayerTypeEnum {
12 m3u8 = 'application/x-mpegURL', 17 m3u8 = 'application/x-mpegURL',
13 mp4 = 'video/mp4', 18 mp4 = 'video/mp4',
@@ -25,6 +30,7 @@ export interface videoListInterface { @@ -25,6 +30,7 @@ export interface videoListInterface {
25 sn: string 30 sn: string
26 channelId: string 31 channelId: string
27 deviceId: string 32 deviceId: string
  33 + customUrl: string
28 params: GBT28181Params 34 params: GBT28181Params
29 } 35 }
30 36
@@ -44,9 +50,9 @@ export const option = { @@ -44,9 +50,9 @@ export const option = {
44 dataset: '', 50 dataset: '',
45 autoplay: true, 51 autoplay: true,
46 poster: '', 52 poster: '',
47 - sourceType: 'custom', 53 + sourceType: sourceTypeEnum.CUSTOM,
48 organization: '', 54 organization: '',
49 - videoId: '' 55 + videoId: null
50 } 56 }
51 57
52 export default class Config extends PublicConfigClass implements CreateComponentType { 58 export default class Config extends PublicConfigClass implements CreateComponentType {
@@ -40,7 +40,7 @@ @@ -40,7 +40,7 @@
40 <setting-item> 40 <setting-item>
41 <n-select 41 <n-select
42 @update:value="handleSelect" 42 @update:value="handleSelect"
43 - v-model:value="url" 43 + v-model:value="optionData.videoId"
44 :options="videoOptions" 44 :options="videoOptions"
45 placeholder="请选择视频地址" 45 placeholder="请选择视频地址"
46 /> 46 />
@@ -56,7 +56,7 @@ @@ -56,7 +56,7 @@
56 56
57 <script setup lang="ts"> 57 <script setup lang="ts">
58 import { PropType, ref, onMounted } from 'vue' 58 import { PropType, ref, onMounted } from 'vue'
59 -import { AccessMode, option, sourceTypeEnum, videoListInterface } from './config' 59 +import { AccessMode, option, sourceTypeEnum, videoListInterface, sourceTypeNameEnum } from './config'
60 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting' 60 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
61 import { NTreeSelect } from 'naive-ui' 61 import { NTreeSelect } from 'naive-ui'
62 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index' 62 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index'
@@ -72,18 +72,16 @@ const props = defineProps({ @@ -72,18 +72,16 @@ const props = defineProps({
72 72
73 const sourceTypes = [ 73 const sourceTypes = [
74 { 74 {
75 - value: 'custom',  
76 - label: '自定义' 75 + value: sourceTypeEnum.CUSTOM,
  76 + label: sourceTypeNameEnum.CUSTOM
77 }, 77 },
78 { 78 {
79 - value: 'platform',  
80 - label: '平台获取' 79 + value: sourceTypeEnum.PLATFORM,
  80 + label: sourceTypeNameEnum.PLATFORM
81 } 81 }
82 ] 82 ]
83 83
84 -const originationOption = ref([])  
85 -  
86 -const url = ref<string | null>(null) 84 +const originationOption = ref<Recordable[]>([])
87 85
88 const videoOptions = ref<videoListInterface[]>([]) 86 const videoOptions = ref<videoListInterface[]>([])
89 87
@@ -103,15 +101,17 @@ const getVideoLists = async (organizationId: string) => { @@ -103,15 +101,17 @@ const getVideoLists = async (organizationId: string) => {
103 videoOptions.value = res?.data?.map((item: videoListInterface) => { 101 videoOptions.value = res?.data?.map((item: videoListInterface) => {
104 return { 102 return {
105 label: item.name, 103 label: item.name,
106 - value: item.accessMode === AccessMode.Streaming ? item.id : item.accessMode === AccessMode.ManuallyEnter ? item.videoUrl : item.id, 104 + value: item.id,
  105 + customUrl: item.accessMode === AccessMode.ManuallyEnter ? item.videoUrl : '', //参数只给自定义视频流使用
107 id: item.id, 106 id: item.id,
108 accessMode: item.accessMode, 107 accessMode: item.accessMode,
109 - channelId: item.accessMode === AccessMode.GBT28181 ? item?.params?.channelNo :'',  
110 - deviceId: item.accessMode === AccessMode.GBT28181 ? item?.params?.deviceId :'', 108 + channelId: item.accessMode === AccessMode.GBT28181 ? item?.params?.channelNo : '', //参数只给gbt28181使用
  109 + deviceId: item.accessMode === AccessMode.GBT28181 ? item?.params?.deviceId : '' //参数只给gbt28181使用
111 } 110 }
112 }) 111 })
113 } 112 }
114 113
  114 +//针对萤石云或者海康威视,根据视频id获取播放流地址
115 const getVideoUrlById = async (id: string) => { 115 const getVideoUrlById = async (id: string) => {
116 const res = await getVideoUrl(id) 116 const res = await getVideoUrl(id)
117 if (!res) return 117 if (!res) return
@@ -119,46 +119,49 @@ const getVideoUrlById = async (id: string) => { @@ -119,46 +119,49 @@ const getVideoUrlById = async (id: string) => {
119 props.optionData.dataset = url 119 props.optionData.dataset = url
120 } 120 }
121 121
  122 +//针对gbt28181,根据设备id和通道号获取播放流地址
  123 +const getVideoControlList = async (deviceId: string, channelId: string) => {
  124 + const {
  125 + data: { flv }
  126 + } = await getVideoControlStart({
  127 + deviceId,
  128 + channelId
  129 + })
  130 + props.optionData.dataset = flv
  131 +}
  132 +
  133 +//针对自定义地址,直接获取地址
  134 +const getCustomUrl = (url: string) => {
  135 + props.optionData.dataset = url
  136 +}
  137 +
122 const handleChecked = (value: string) => { 138 const handleChecked = (value: string) => {
123 props.optionData.dataset = '' 139 props.optionData.dataset = ''
124 props.optionData.organization = '' 140 props.optionData.organization = ''
125 - url.value = null 141 + props.optionData.videoId = null
126 if (value === sourceTypeEnum.PLATFORM) { 142 if (value === sourceTypeEnum.PLATFORM) {
127 getOriginationList() 143 getOriginationList()
128 } 144 }
129 } 145 }
130 146
131 -const getVideoControlList = async (deviceId: string, channelId: string) => {  
132 - const { data: { flv } } = await getVideoControlStart({  
133 - deviceId,  
134 - channelId  
135 - })  
136 - props.optionData.dataset = flv || ''  
137 -}  
138 -  
139 -const handleSelect = (_: string, e: videoListInterface) => {  
140 - const { accessMode, id, value , channelId, deviceId } = e  
141 - //流媒体,需要从服务端调取接口换取播放的地址 147 +const handleSelect = (_: string, e: videoListInterface) => {
  148 + const { accessMode, id, customUrl, channelId, deviceId, value } = e
  149 + props.optionData.videoId = value as any
142 if (accessMode === AccessMode.Streaming) { 150 if (accessMode === AccessMode.Streaming) {
143 getVideoUrlById(id) 151 getVideoUrlById(id)
144 - url.value = id  
145 - props.optionData.videoId = id  
146 } else if (accessMode === AccessMode.GBT28181) { 152 } else if (accessMode === AccessMode.GBT28181) {
147 - //gbt28181,需要调用接口获取flv播放地址  
148 getVideoControlList(deviceId, channelId) 153 getVideoControlList(deviceId, channelId)
149 - props.optionData.videoId = id  
150 } else { 154 } else {
151 - props.optionData.dataset = value as string 155 + getCustomUrl(customUrl)
152 } 156 }
153 } 157 }
154 158
155 -onMounted(async () => { 159 +onMounted(() => {
156 if (props.optionData.sourceType === sourceTypeEnum.PLATFORM) { 160 if (props.optionData.sourceType === sourceTypeEnum.PLATFORM) {
157 getOriginationList() 161 getOriginationList()
158 if (props.optionData.organization) { 162 if (props.optionData.organization) {
159 - await getVideoLists(props.optionData.organization) 163 + getVideoLists(props.optionData.organization)
160 } 164 }
161 - url.value = props.optionData.videoId  
162 } 165 }
163 }) 166 })
164 167