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 3 import { CameraConfig } from './index'
4 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 13 export enum sourceTypeEnum {
7 14 CUSTOM = 'custom',
8 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 58 export const option = {
12   - autoSwitch: false,
  59 + autoSwitch: false, //开启切换
  60 + interval: 2000, // 自动播放的间隔(ms)
13 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 72 export default class Config extends PublicConfigClass implements CreateComponentType {
... ...
... ... @@ -11,11 +11,16 @@
11 11 </setting-item>
12 12 </setting-item-box>
13 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 19 <setting-item-box name="源类型" :alone="true">
15 20 <setting-item>
16 21 <n-radio-group @change="handleChecked($event, item, index)" v-model:value="item.sourceType" name="radiogroup">
17 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 24 {{ item.label }}
20 25 </n-radio>
21 26 </n-space>
... ... @@ -45,8 +50,8 @@
45 50 <setting-item-box v-if="item.sourceType === sourceTypeEnum.PLATFORM" name="视频" :alone="true">
46 51 <setting-item>
47 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 55 :options="videoOptions"
51 56 />
52 57 </setting-item>
... ... @@ -57,7 +62,15 @@
57 62 style="margin-left: 10px"
58 63 v-if="optionData.dataset.length < 9"
59 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 76 </n-button>
... ... @@ -66,10 +79,11 @@
66 79
67 80 <script setup lang="ts">
68 81 import { PropType, ref, onMounted } from 'vue'
69   -import { option, sourceTypeEnum } from './config'
  82 +import { AccessModeEnum, datasetList, option, sourceTypeEnum, sourceTypeNameEnum, videoList } from './config'
70 83 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
71 84 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index'
72 85 import { NTreeSelect } from 'naive-ui'
  86 +import { getVideoControlStart } from '@/api/external/flvPlay'
73 87
74 88 const props = defineProps({
75 89 optionData: {
... ... @@ -80,66 +94,99 @@ const props = defineProps({
80 94
81 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 110 const getOriginationList = async () => {
97 111 const res = await getOrganizationList()
98 112 originationOption.value = res
99 113 }
100 114
101   -const handleUpdateTreeValue = (e: any) => {
  115 +const handleUpdateTreeValue = (e: string) => {
102 116 getVideoLists(e)
103 117 }
104 118
105 119 const getVideoLists = async (organizationId: string) => {
106 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 123 label: item.name,
109   - value: item.accessMode === 1 ? item.id : item.videoUrl,
  124 + value: item.id,
110 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 135 const res = await getVideoUrl(id)
125 136 if (!res) return
126 137 const { url } = res.data
127   - props.optionData.dataset.forEach((item: any, itemIndex: number) => {
  138 + props.optionData.dataset.forEach((item: datasetList, itemIndex: number) => {
128 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 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 188 onMounted(() => {
142   - props.optionData.dataset.forEach((item: any) => {
  189 + props.optionData.dataset.forEach((item: datasetList) => {
143 190 if (item.sourceType === sourceTypeEnum.PLATFORM) {
144 191 getOriginationList()
145 192 if (item.organization) {
... ...
1 1 <template>
2 2 <div @mouseenter="handleMouseenter" @mouseleave="handleMouseleave" class="banner-box" ref="root">
3 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 7 :name="item.name"
8 8 :avatar="item.avatar"
9 9 :key="item + index"
10   - :sourceSrc="!item.byIdUrl ? item.url : item.byIdUrl"
  10 + :sourceSrc="item.url"
11 11 :w="w"
12 12 :h="h"
13 13 :index="index"
... ... @@ -23,8 +23,8 @@
23 23 import { PropType, watch, toRefs, shallowReactive, onMounted, ref } from 'vue'
24 24 import { CreateComponentType } from '@/packages/index.d'
25 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 29 const props = defineProps({
30 30 chartConfig: {
... ... @@ -39,15 +39,14 @@ const { w, h } = toRefs(props.chartConfig.attr)
39 39
40 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 47 let initial = ref(0)
49 48
50   -const computedFunc = (initial: number, source: any) => {
  49 +const computedFunc = (initial: number, source: Recordable[]) => {
51 50 if (initial < 0) initial = 0
52 51 if (Array.isArray(source)) {
53 52 let len = source.length,
... ... @@ -56,14 +55,14 @@ const computedFunc = (initial: number, source: any) => {
56 55 temp3 = initial,
57 56 temp4 = initial + 1 >= len ? initial + 1 - len : initial + 1,
58 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 59 let transform = `translateX(-50%) scale(0.7)`,
61 60 zIndex = 0,
62 61 className = 'slide'
63 62 switch (index) {
64 63 case temp3:
65 64 transform = `translateX(-50%) scale(1)`
66   - className = ['slide', 'activate'] as any
  65 + className = ['slide', 'activate'] as string[] & string
67 66 zIndex = 300
68 67 break
69 68 case temp1:
... ... @@ -114,6 +113,7 @@ watch(
114 113 )
115 114
116 115 // 处理自动轮播
  116 +// 暂定 any类型
117 117 let timer: any = null
118 118
119 119 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 131 onMounted(() => {
132 132 clearInterval(timer)
133   - const box: any = root.value
134   - box.onmouseenter = () => clearInterval(timer)
  133 + root.value!.onmouseenter = () => clearInterval(timer)
135 134 if (autoSwitch.value) {
136 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 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 171 const handleMouseenter = () => {
159 172 isShowSvg.value = true
160 173 }
... ...
... ... @@ -9,7 +9,7 @@
9 9 </div>
10 10 </template>
11 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 13 import videojs from 'video.js'
14 14 import 'videojs-flvjs-es6'
15 15 import type { VideoJsPlayerOptions } from 'video.js'
... ... @@ -45,6 +45,7 @@ const isRtspProtocol = (url: string) => {
45 45 }
46 46
47 47 const getVideoTypeByUrl = (url = '') => {
  48 + if(!url) return;
48 49 try {
49 50 const { protocol, pathname } = new URL(url)
50 51 if (protocol.startsWith('rtsp:')) return VideoPlayerTypeEnum.flv
... ... @@ -112,6 +113,7 @@ async function getSource() {
112 113
113 114 // 初始化videojs
114 115 const initVideo = async () => {
  116 + await nextTick()
115 117 if (videoRef.value) {
116 118 // 创建 video 实例
117 119 options.sources = await getSource()
... ... @@ -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 153 watch(
150 154 () => props.sourceSrc,
151 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 164 videoPlayer?.src({
153 165 type: getVideoTypeByUrl(props.sourceSrc),
154 166 src: props.sourceSrc!
155 167 });
156 168 videoPlayer?.load();
157   - videoPlayer?.play()
  169 + videoPlayer?.play();
158 170 },
159 171 {
160 172 immediate: true
... ... @@ -170,9 +182,6 @@ watch(
170 182 videoPlayer?.pause()
171 183 }
172 184 },
173   - {
174   - immediate: true
175   - }
176 185 )
177 186
178 187 onMounted(() => {
... ... @@ -191,10 +200,7 @@ const handleVideoPlay = () => videoPlayer?.play()
191 200
192 201 //暂停和销毁
193 202 const handleVideoDispose = () => videoPlayer?.dispose() && videoPlayer?.pause()
194   -defineExpose({
195   - handleVideoPlay,
196   - handleVideoDispose
197   -})
  203 +
198 204 </script>
199 205
200 206 <style lang="scss" scoped>
... ...
... ... @@ -8,6 +8,11 @@ export enum sourceTypeEnum {
8 8 PLATFORM = 'platform'
9 9 }
10 10
  11 +export enum sourceTypeNameEnum {
  12 + CUSTOM = '自定义',
  13 + PLATFORM = '平台获取'
  14 +}
  15 +
11 16 export enum VideoPlayerTypeEnum {
12 17 m3u8 = 'application/x-mpegURL',
13 18 mp4 = 'video/mp4',
... ... @@ -25,6 +30,7 @@ export interface videoListInterface {
25 30 sn: string
26 31 channelId: string
27 32 deviceId: string
  33 + customUrl: string
28 34 params: GBT28181Params
29 35 }
30 36
... ... @@ -44,9 +50,9 @@ export const option = {
44 50 dataset: '',
45 51 autoplay: true,
46 52 poster: '',
47   - sourceType: 'custom',
  53 + sourceType: sourceTypeEnum.CUSTOM,
48 54 organization: '',
49   - videoId: ''
  55 + videoId: null
50 56 }
51 57
52 58 export default class Config extends PublicConfigClass implements CreateComponentType {
... ...
... ... @@ -40,7 +40,7 @@
40 40 <setting-item>
41 41 <n-select
42 42 @update:value="handleSelect"
43   - v-model:value="url"
  43 + v-model:value="optionData.videoId"
44 44 :options="videoOptions"
45 45 placeholder="请选择视频地址"
46 46 />
... ... @@ -56,7 +56,7 @@
56 56
57 57 <script setup lang="ts">
58 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 60 import { CollapseItem, SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
61 61 import { NTreeSelect } from 'naive-ui'
62 62 import { getOrganizationList, getVideoList, getVideoUrl } from '@/api/external/common/index'
... ... @@ -72,18 +72,16 @@ const props = defineProps({
72 72
73 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 86 const videoOptions = ref<videoListInterface[]>([])
89 87
... ... @@ -103,15 +101,17 @@ const getVideoLists = async (organizationId: string) => {
103 101 videoOptions.value = res?.data?.map((item: videoListInterface) => {
104 102 return {
105 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 106 id: item.id,
108 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 115 const getVideoUrlById = async (id: string) => {
116 116 const res = await getVideoUrl(id)
117 117 if (!res) return
... ... @@ -119,46 +119,49 @@ const getVideoUrlById = async (id: string) => {
119 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 138 const handleChecked = (value: string) => {
123 139 props.optionData.dataset = ''
124 140 props.optionData.organization = ''
125   - url.value = null
  141 + props.optionData.videoId = null
126 142 if (value === sourceTypeEnum.PLATFORM) {
127 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 150 if (accessMode === AccessMode.Streaming) {
143 151 getVideoUrlById(id)
144   - url.value = id
145   - props.optionData.videoId = id
146 152 } else if (accessMode === AccessMode.GBT28181) {
147   - //gbt28181,需要调用接口获取flv播放地址
148 153 getVideoControlList(deviceId, channelId)
149   - props.optionData.videoId = id
150 154 } else {
151   - props.optionData.dataset = value as string
  155 + getCustomUrl(customUrl)
152 156 }
153 157 }
154 158
155   -onMounted(async () => {
  159 +onMounted(() => {
156 160 if (props.optionData.sourceType === sourceTypeEnum.PLATFORM) {
157 161 getOriginationList()
158 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
... ...