Video.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <template>
  2. <div
  3. class="o-video"
  4. :class="{ offline: !online }"
  5. @mouseenter.stop="mouseOver"
  6. @mouseleave.stop="mouseLeave"
  7. >
  8. <video
  9. v-if="online"
  10. ref="player"
  11. class="o-video__player o-simple-video"
  12. muted
  13. autoplay
  14. :poster="poster"
  15. />
  16. <div
  17. v-else
  18. class="o-video__offline"
  19. :style="{ zoom: zoomnum }"
  20. >
  21. 当前设备离线了
  22. </div>
  23. <div
  24. class="o-video__tip"
  25. :class="{ online }"
  26. />
  27. <div class="c-footer u-ellipsis">{{ device.name }}</div>
  28. <div
  29. v-show="maskShow"
  30. class="o-video__mask"
  31. >
  32. <div
  33. class="o-video__btn u-pointer"
  34. :style="{ zoom: zoomnum }"
  35. @click="onPlay"
  36. >
  37. <i
  38. class="o-video__icon has-bg"
  39. :class="iconClass"
  40. />
  41. </div>
  42. </div>
  43. </div>
  44. </template>
  45. <script>
  46. import flvjs from 'flv.js'
  47. import { authCode } from '@/api/camera'
  48. import videoPoster from '@/assets/video-poster.png'
  49. const CAMERA_URL = `${location.protocol}//${process.env.VUE_APP_GATEWAY || location.host}`
  50. export default {
  51. name: 'DeviceCard',
  52. props: {
  53. device: {
  54. type: Object,
  55. required: true
  56. },
  57. zoomnum: {
  58. type: String,
  59. default: null
  60. }
  61. },
  62. data () {
  63. return {
  64. poster: videoPoster,
  65. maskShow: false,
  66. refreshShow: false,
  67. first: false
  68. }
  69. },
  70. computed: {
  71. iconClass () {
  72. if (this.refreshShow) {
  73. return 'refresh'
  74. }
  75. return this.device.paused ? 'paused' : 'playing'
  76. },
  77. online () {
  78. return this.device.onlineStatus === 1
  79. }
  80. },
  81. created () {
  82. if (this.online) {
  83. this.$timer = -1
  84. this.getAuthCode()
  85. }
  86. },
  87. beforeDestroy () {
  88. if (this.$player) {
  89. clearTimeout(this.$timer)
  90. this.destroyPlay()
  91. }
  92. },
  93. methods: {
  94. mouseOver () {
  95. if (this.online) {
  96. this.maskShow = true
  97. }
  98. if (this.$player && !this.$refs.player.paused) {
  99. this.device.paused = false
  100. } else {
  101. this.device.paused = true
  102. }
  103. },
  104. mouseLeave () {
  105. this.maskShow = false
  106. },
  107. onPlay () {
  108. this.first = true
  109. if (this.$refs.player.paused) {
  110. this.maskShow = false
  111. }
  112. this.refreshShow = false
  113. if (this.$player && !this.$refs.player.paused) {
  114. this.device.paused = true
  115. this.$player.pause()
  116. } else {
  117. if (this.$player) {
  118. this.destroyPlay()
  119. }
  120. this.device.paused = false
  121. this.getAuthCode()
  122. }
  123. },
  124. getAuthCode () {
  125. authCode({ deviceId: this.device.id }).then(({ data }) => {
  126. this.createPlayer(data)
  127. })
  128. },
  129. createPlayer ({ timestamp, token, expire }) {
  130. if (flvjs.isSupported()) {
  131. // 创建一个flvjs实例
  132. const url = `${CAMERA_URL}/live/${this.device.id}.flv?authorization=${token}&timestamp=${timestamp}&expire=${expire}`
  133. this.$player = flvjs.createPlayer({
  134. type: 'flv',
  135. isLive: true,
  136. hasAudio: false,
  137. url
  138. })
  139. this.$player.on('error', () => {
  140. this.refreshShow = true
  141. this.destroyPlay()
  142. this.$message({
  143. type: 'warning',
  144. message: '设备视频流出错'
  145. })
  146. })
  147. let decodedFrames = -1
  148. this.$player.on('statistics_info', res => {
  149. decodedFrames = res.decodedFrames
  150. })
  151. this.$timer = setTimeout(() => {
  152. if (decodedFrames === 0) {
  153. this.destroyPlay()
  154. this.$message({
  155. type: 'warning',
  156. message: `${this.device.name}设备没有视频流`
  157. })
  158. this.refreshShow = true
  159. }
  160. }, 10000)
  161. // 将实例挂载到video元素上面
  162. this.$player.attachMediaElement(this.$refs.player)
  163. try {
  164. // 开始运行加载 只要流地址正常 就可以在h5页面中播放出画面了
  165. this.$player.load()
  166. this.$player.play()
  167. if (!this.first) {
  168. this.$player.pause()
  169. }
  170. } catch (error) {
  171. console.log('连接websocker异常', error)
  172. }
  173. }
  174. },
  175. destroyPlay () {
  176. this.$player.pause()
  177. this.$player.unload()
  178. this.$player.detachMediaElement()
  179. this.$player.destroy()
  180. this.$player = null
  181. }
  182. }
  183. }
  184. </script>
  185. <style lang="scss" scoped>
  186. .o-video {
  187. position: relative;
  188. padding-top: 56.25%;
  189. border-radius: $radius--mini;
  190. overflow: hidden;
  191. &.offline {
  192. background: url("~@/assets/image_offline.svg") 0 0 / 100% 100% no-repeat;
  193. }
  194. &__player {
  195. position: absolute;
  196. top: 0;
  197. left: 0;
  198. width: 100%;
  199. height: 100%;
  200. }
  201. &__offline {
  202. position: absolute;
  203. top: 73%;
  204. left: 50%;
  205. color: $info;
  206. font-size: 24px;
  207. transform: translateX(-50%);
  208. }
  209. &__tip {
  210. position: absolute;
  211. top: 0;
  212. right: 0;
  213. width: 48px;
  214. height: 24px;
  215. color: #fff;
  216. font-size: 14px;
  217. line-height: 24px;
  218. text-align: center;
  219. border-radius: 0 0 0 $radius--mini;
  220. background-color: $error--dark;
  221. z-index: 9;
  222. &::after {
  223. content: "离线";
  224. }
  225. &.online {
  226. background-color: $success--dark;
  227. &::after {
  228. content: "在线";
  229. }
  230. }
  231. }
  232. &__mask {
  233. position: absolute;
  234. top: 0;
  235. left: 0;
  236. width: 100%;
  237. height: 100%;
  238. background-color: rgba(0, 0, 0, 0.3);
  239. z-index: 2;
  240. }
  241. &__btn {
  242. position: absolute;
  243. left: 50%;
  244. top: 50%;
  245. width: 128px;
  246. padding-top: 128px;
  247. border-radius: 50%;
  248. background-color: rgba(#f4f7fb, 0.5);
  249. transform: translate(-50%, -50%);
  250. }
  251. &__icon {
  252. position: absolute;
  253. top: 50%;
  254. left: 50%;
  255. transform: translate(-50%, -50%);
  256. &.playing {
  257. border-top: 22px solid transparent;
  258. border-left: 37px solid #fff;
  259. border-bottom: 22px solid transparent;
  260. transform: translate(-15px, -50%);
  261. }
  262. &.paused {
  263. width: 40%;
  264. height: 60%;
  265. border-left: 10px solid #fff;
  266. border-right: 10px solid #fff;
  267. }
  268. &.refresh {
  269. width: 60%;
  270. height: 60%;
  271. background-image: url("~@/assets/icon_refresh.png");
  272. }
  273. }
  274. }
  275. </style>