ソースを参照

[FE]feat:增加视频回放组件

chenlp5 4 年 前
コミット
c302c0edf8

+ 6 - 4
security-protection-platform/package.json

@ -61,8 +61,7 @@
61 61
    "stylelint-order": "^0.7.0",
62 62
    "stylelint-scss": "^2.1.0",
63 63
    "vue-loader": "^14.2.3",
64
    "vue-template-compiler": "^2.2.1",
65
    "vuedraggable": "2.19"
64
    "vue-template-compiler": "^2.2.1"
66 65
  },
67 66
  "dependencies": {
68 67
    "aid-elements-desktop": "latest",
@ -75,6 +74,9 @@
75 74
    "vuex": "^2.3.1",
76 75
    "vuex-router-sync": "^4.1.2",
77 76
    "js-cookie": "",
78
    "element-ui": "2.14.1"
77
    "element-ui": "2.14.1",
78
    "vuedraggable": "2.19",
79
    "video.js": "7.10.2",
80
    "videojs-playlist": "4.3.1"
79 81
  }
80
}
82
}

+ 128 - 0
security-protection-platform/src/components/VideoPlayer/index.vue

@ -0,0 +1,128 @@
1
<template>
2
  <div>
3
    <video ref="player" class="video-js vjs-default-skin vjs-big-play-centered"></video>
4
  </div>
5
</template>
6
7
<script>
8
import videojs from 'video.js'
9
10
window.videojs = videojs
11
12
require('videojs-playlist')
13
require('video.js/dist/lang/zh-CN.js')
14
require('video.js/dist/video-js.min.css')
15
16
const defaultPlayerOptions = {
17
  controls: true,
18
  autoplay: false,
19
  muted: false,
20
  language: 'zh-CN',
21
  fluid: true,
22
  // liveui: true,
23
  controlBar: {
24
    children: [
25
      {name: 'playToggle'}, // 播放按钮
26
      {name: 'currentTimeDisplay'}, // 当前已播放时间
27
      {name: 'progressControl'}, // 播放进度条
28
      {name: 'durationDisplay'}, // 总时间
29
      { // 倍数播放
30
        name: 'playbackRateMenuButton',
31
        playbackRates: [0.5, 1, 1.5, 2, 2.5]
32
      },
33
      {
34
        name: 'volumePanel', // 音量控制
35
        inline: false // 不使用水平方式
36
      },
37
      {name: 'FullscreenToggle'} // 全屏
38
    ]
39
  }
40
}
41
42
export default {
43
  name: 'VideoPlayer',
44
  props: {
45
    sources: {
46
      type: Array,
47
      default: () => []
48
    },
49
    options: {
50
      type: Object,
51
      default() { return {} }
52
    },
53
    autoadvance: {
54
      type: Number,
55
      default: null
56
    }
57
  },
58
  data() {
59
    const playerOptions = Object.assign({}, defaultPlayerOptions, this.options)
60
    return {
61
      player: null,
62
      playerOptions
63
    }
64
  },
65
  computed: {
66
    playlist() {
67
      return this.sources
68
    }
69
  },
70
  watch: {
71
    playlist: {
72
      handler(list) {
73
        if (!this.player) return
74
        this.player.playlist(list)
75
        if (list.length > 0) {
76
          this.player.playlist.autoadvance(this.autoadvance)
77
          // this.player.playlist.first();
78
        }
79
        this.$emit('playlistchange', list)
80
      },
81
      immediate: true
82
    }
83
  },
84
  mounted() {
85
    this.init()
86
  },
87
  beforeDestroy() {
88
    if (this.player) {
89
      this.player.dispose()
90
    }
91
  },
92
  methods: {
93
    init() {
94
      const player = videojs(this.$refs.player, this.playerOptions)
95
      this.player = player
96
      this.player.on('playlistitem', (...args) => {
97
        this.$emit('playlistitem', ...args)
98
      })
99
      this.$emit('player-inited', player)
100
    }
101
  }
102
}
103
</script>
104
105
<style lang="scss">
106
107
.vjs-paused .vjs-big-play-button,
108
.vjs-paused.vjs-has-started .vjs-big-play-button {
109
    display: block;
110
}
111
112
.vjs-slider-vertical .vjs-volume-level:before {
113
  top: -.4em;
114
  left: -.4em;
115
}
116
117
.video-js {
118
  .vjs-play-progress:before {
119
    top: -.5em;
120
    transform-origin: center;
121
  }
122
  .vjs-progress-control:hover {
123
      .vjs-play-progress:before {
124
        transform: translateY(0.17em);
125
      }
126
    }
127
}
128
</style>

+ 173 - 0
security-protection-platform/src/modules/videoSurveillance/components/ReplayDialog/index.vue

@ -0,0 +1,173 @@
1
<template>
2
  <t-modal :visibled.sync="visible" :header-visibled="false" :footer-visibled="false" :no-padding="true" width="100%">
3
    <div class="replayer">
4
      <div class="replayer-list">
5
        <t-tabs v-model="currentTab" mode="scrollY" orientation="vertical" width="150px" @change="handleTabChange">
6
          <t-tab-panel v-for="item in list" :key="item.fileId" :label="item.fileName" :panel-id="item.fileId" />
7
        </t-tabs>
8
      </div>
9
10
      <div class="replayer-video">
11
        <t-loading v-model="loadVideo">
12
          <span class="text-md text-info">获取视频资源...</span>
13
        </t-loading>
14
        <video-player
15
          :sources="videoList"
16
          :autoadvance="0"
17
          :style="{opacity: loadVideo ? 0: 1}"
18
          @player-inited="handlePlayerInited"
19
          @playlistitem="handlePlayerCurrentChange"
20
        />
21
      </div>
22
    </div>
23
  </t-modal>
24
</template>
25
26
<script>
27
import VideoPlayer from '@/components/VideoPlayer'
28
29
export default {
30
  components: { VideoPlayer },
31
  props: {
32
    list: {
33
      type: Array,
34
      default: () => []
35
    },
36
    visibled: {
37
      type: Boolean,
38
      default: false
39
    }
40
  },
41
  data() {
42
    return {
43
      currentTab: null,
44
      loadVideo: false,
45
      videoList: [],
46
      $player: null
47
    }
48
  },
49
  computed: {
50
    visible: {
51
      get() { return this.visibled },
52
      set(val) {
53
        if (!val) this.$player.pause()
54
        this.$emit('update:visibled', val)
55
      }
56
    }
57
  },
58
  watch: {
59
    list: {
60
      handler(val) {
61
        console.warn(`list Change`)
62
        this.resetVideoList()
63
        if (val.length > 0) {
64
          this.currentTab = val[0].fileId
65
          // 手动触发第一个tab点击事件
66
          this.handleTabChange(null, 0)
67
        }
68
      },
69
      immediate: true
70
    }
71
  },
72
  methods: {
73
    handlePlayerInited(player) {
74
      this.$player = player
75
      window.player = this
76
    },
77
    async handleTabChange(name, index) {
78
      this.$player.pause()
79
      await this.preloadOriVideoUrl(index)
80
      this.play(index)
81
    },
82
    handlePlayerCurrentChange(e, item) {
83
      const { index, fileId } = item
84
      this.currentTab = fileId
85
      this.preloadOriVideoUrl(index)
86
    },
87
    resetVideoList() {
88
      const videoList = this.list.map((item, index) => {
89
        const { fileId, fileType } = item
90
        return {
91
          index,
92
          fileId,
93
          fileType,
94
          loaded: false
95
        }
96
      })
97
98
      this.videoList = videoList
99
    },
100
    play(index) {
101
      console.log(`播放:index=${index}`)
102
      // 调用额外的currentItem方法以避免currentItem在首次不生效
103
      // https://github.com/brightcove/videojs-playlist/blob/master/docs/api.md
104
      this.$player.playlist.currentItem()
105
      this.$player.playlist.currentItem(index)
106
      this.$player.play()
107
    },
108
    /**
109
     *  从index开始预载videoList的视频地址
110
     *  此方法将在获取index对应的地址后就立即返回
111
     */
112
    preloadOriVideoUrl(index = 0, maxLength = 5) {
113
      return new Promise((resolve, reject) => {
114
        for (let i = index; i in this.videoList && i - index < maxLength; i++) {
115
          console.log(`预载地址:index=${i}`)
116
          const item = this.videoList[i]
117
          const task = item.loaded
118
            ? Promise.resolve()
119
            : this.getOriVideoUrl(item.fileId).then(resp => {
120
              console.log(`预载完成,更新播放源:index=${i}`)
121
              item.loaded = true
122
              item.sources = [{
123
                src: resp.url,
124
                type: item.fileType
125
              }]
126
            })
127
          if (i === index) {
128
            this.loadVideo = !item.loaded
129
            task.then(() => {
130
              resolve()
131
            }).finally(() => {
132
              this.loadVideo = false
133
            })
134
          }
135
        }
136
      })
137
    },
138
    /*
139
     * 获取真实视频地址
140
     */
141
    getOriVideoUrl(fileId) {
142
      console.log(`请求地址:${fileId}`)
143
      // fake
144
      return new Promise((resolve, reject) => {
145
        setTimeout(() => {
146
          const x = Math.random()
147
          const url = x > 0.5
148
            ? 'http://10.19.90.34:19000/tool-image/tool-image_7d359725fac4464fb248284caf321993.mp4'
149
            : 'http://10.19.90.34:19000/tool-image/tool-image_7fa1f7b30f0640f2a67ac8b4c2e0b574.mp4'
150
          resolve({
151
            url
152
          })
153
        }, 1000)
154
      })
155
    }
156
  }
157
}
158
</script>
159
160
<style lang="scss">
161
.replayer {
162
  display: flex;
163
  &-list {
164
    margin-right: 20px;
165
    // width: 200px;
166
  }
167
168
  &-video {
169
    position: relative;
170
    flex: auto;
171
  }
172
}
173
</style>

+ 59 - 25
security-protection-platform/src/modules/videoSurveillance/index.vue

@ -12,13 +12,13 @@
12 12
    </div>
13 13
    <div class="page-bottom">
14 14
      <div v-for="(item,index) in videoList" :key="index">
15
        <t-card :class="index%3===0?'card-video-first':'card-video'">
15
        <t-card class="card-video">
16 16
          <img :src="item.url" class="card-image">
17 17
          <div slot="foot" class="bottom">
18 18
            <span v-tooltip="item.videoDetail">{{ item.videoDetail.content | handleText }}</span>
19 19
            <div style="margin-left:auto">
20 20
              <t-button v-tooltip="item.options1" class="bottom-btn" @click="goDistinguishRecord(item.videoId)"><t-icon size="16" icon="image-outline"></t-icon></t-button>
21
              <t-button v-tooltip="item.options2" class="bottom-btn"><t-icon size="16" icon="piechart-outline"></t-icon></t-button>
21
              <t-button v-tooltip="item.options2" class="bottom-btn" @click="handleReview(item.videoId)"><t-icon size="16" icon="piechart-outline"></t-icon></t-button>
22 22
              <t-button v-tooltip="item.options3" class="bottom-btn"><t-icon size="16" icon="arrow-expand-all-outline"></t-icon></t-button>
23 23
            </div>
24 24
          </div>
@ -26,12 +26,16 @@
26 26
      </div>
27 27
    </div>
28 28
    <t-pager :page-size="videoPageSize" :current="videoCurrent" :total="videoTotal" :sizer-range="[ 5, 10, 20, 30 ]" class="pager" show-elevator @on-change="onChangeGate"></t-pager>
29
    <replay-dialog :list="replayList" :visibled.sync="showReplayDialog" />
29 30
  </div>
30 31
</template>
31 32
<script>
32 33
import sysapi from '@/api/videoSurveillance'
33 34
import commonapi from '@/api/common'
35
import ReplayDialog from './components/ReplayDialog'
36
34 37
export default {
38
  components: { ReplayDialog },
35 39
  filters: {
36 40
    handleText(value) {
37 41
      if (!value) return ''
@ -56,7 +60,10 @@ export default {
56 60
      gateFieldData: 101101,
57 61
      videoList: [],
58 62
      totalList: [],
59
      roomList: []
63
      roomList: [],
64
65
      showReplayDialog: false,
66
      replayList: []
60 67
    }
61 68
  },
62 69
  mounted() {
@ -64,6 +71,45 @@ export default {
64 71
    this.getVideoSurveillanceData() // 获取视频监控界面数据
65 72
  },
66 73
  methods: {
74
    handleReview() {
75
      this.replayList = [{
76
        fileName: '12月14 16:55',
77
        fileId: 'ai-video_5A02296PAKA885B-video20201214165526.mp4',
78
        fileType: 'video/mp4'
79
      }, {
80
        fileName: '12月14 16:56',
81
        fileId: 'ai-video_5A02296PAKA885B-video20201214165527.mp4',
82
        fileType: 'video/mp4'
83
      }, {
84
        fileName: '12月14 16:57',
85
        fileId: 'ai-video_5A02296PAKA885B-video20201214165528.mp4',
86
        fileType: 'video/mp4'
87
      }, {
88
        fileName: '12月14 16:58',
89
        fileId: 'ai-video_5A02296PAKA885B-video20201214165529.mp4',
90
        fileType: 'video/mp4'
91
      }, {
92
        fileName: '12月14 16:59',
93
        fileId: 'ai-video_5A02296PAKA885B-video20201214165530.mp4',
94
        fileType: 'video/mp4'
95
      }, {
96
        fileName: '12月14 17:00',
97
        fileId: 'ai-video_5A02296PAKA885B-video20201214165531.mp4',
98
        fileType: 'video/mp4'
99
      }]
100
      // this.replayList = [{
101
      //   sources: [{
102
      //     src: 'http://10.19.90.34:19000/tool-image/tool-image_7fa1f7b30f0640f2a67ac8b4c2e0b574.mp4',
103
      //     type: 'video/mp4'
104
      //   }]
105
      // }, {
106
      //   sources: [{
107
      //     src: 'http://10.19.90.34:19000/tool-image/tool-image_7fa1f7b30f0640f2a67ac8b4c2e0b574.mp4',
108
      //     type: 'video/mp4'
109
      //   }]
110
      // }]
111
      this.showReplayDialog = true
112
    },
67 113
    async tabClick(id) {
68 114
      this.paramsObj.page = 0
69 115
      this.videoList = []
@ -136,9 +182,10 @@ export default {
136 182
        display: flex;
137 183
        width: 100%;
138 184
        flex-wrap: wrap;
185
        margin-left: -24px;
139 186
        .card-video-first, .card-video{
140
          width:397px;
141
          height:245px;
187
          width:354px;
188
          height:240px;
142 189
          margin-top: 20px;
143 190
            .card-image{
144 191
                width: 100%;
@ -162,28 +209,15 @@ export default {
162 209
                }
163 210
            }
164 211
        }
165
        .card-video-first{
166
          .card-footer{
167
             padding: 0px !important;
168
             background: none;
169
             width: 100%;
170
             position: absolute;
171
             border-top: none;
172
             bottom: 0px;
173
             }
174
            .card-block{
175
              padding:0 !important;
176
            }
177
        }
178 212
        .card-video{
179 213
          .card-footer{
180
             padding: 0px 0 0 24px !important;
181
             background: none;
182
             width: 100%;
183
             position: absolute;
184
             border-top: none;
185
             bottom: 0px;
186
             }
214
            padding: 0px 0 0 24px !important;
215
            background: none;
216
            width: 100%;
217
            position: absolute;
218
            border-top: none;
219
            bottom: 0px;
220
            }
187 221
            .card-block, .card-video-block{
188 222
              margin-left: 24px;
189 223
              padding:0 !important;

+ 1 - 0
security-protection-platform/src/styles/index.scss

@ -25,6 +25,7 @@ html {
25 25
*,
26 26
*:before,
27 27
*:after {
28
  font-family: inherit;
28 29
  box-sizing: inherit;
29 30
}
30 31