Browse Source

feat: support live

Casper Dai 3 years ago
parent
commit
6c415716db

+ 1 - 0
package.json

@@ -16,6 +16,7 @@
     "dom-to-image": "^2.6.0",
     "dom-to-image": "^2.6.0",
     "echarts": "^5.2.1",
     "echarts": "^5.2.1",
     "element-ui": "^2.15.6",
     "element-ui": "^2.15.6",
+    "hls.js": "^1.1.2",
     "js-cookie": "^3.0.1",
     "js-cookie": "^3.0.1",
     "keycloak-js": "^15.0.2",
     "keycloak-js": "^15.0.2",
     "mediainfo.js": "^0.1.7",
     "mediainfo.js": "^0.1.7",

+ 4 - 3
src/views/bigscreen/core/base.js

@@ -56,7 +56,7 @@ export default {
         if (this.node.bgm.length) {
         if (this.node.bgm.length) {
           return true
           return true
         }
         }
-        return this.node.widgets.some(widget => widget.mute === 0 && widget.sources.length)
+        return this.node.widgets.some(widget => widget.mute === 0 && (widget.sources.length || widget.url))
       }
       }
       return false
       return false
     },
     },
@@ -273,10 +273,11 @@ export default {
       this.snapping = true
       this.snapping = true
       return domToImage.toJpeg(this.$refs.wrapper, {
       return domToImage.toJpeg(this.$refs.wrapper, {
         filter (node) {
         filter (node) {
-          if (node.tagName === 'CANVAS') {
+          const { tagName } = node
+          if (tagName === 'CANVAS') {
             return /^data:.+;base64,.+/.test(node.toDataURL())
             return /^data:.+;base64,.+/.test(node.toDataURL())
           }
           }
-          return node.tagName !== 'VIDEO'
+          return tagName !== 'VIDEO' && tagName !== 'IFRAME'
         },
         },
         width: this.node.width * this.scale | 0,
         width: this.node.width * this.scale | 0,
         height: this.node.height * this.scale | 0,
         height: this.node.height * this.scale | 0,

+ 29 - 1
src/views/bigscreen/core/components/DynamicItem.vue

@@ -26,10 +26,13 @@
     />
     />
     <el-input
     <el-input
       v-if="isTextArea"
       v-if="isTextArea"
-      v-model="node[attrKey]"
+      v-model="inputValue"
       class="c-dynamic-item__value"
       class="c-dynamic-item__value"
       type="textarea"
       type="textarea"
+      size="mini"
       placeholder="请输入内容"
       placeholder="请输入内容"
+      @keydown.enter.native="onKeyEnter($event)"
+      @change="onChange"
     />
     />
     <el-select
     <el-select
       v-if="isSelect"
       v-if="isSelect"
@@ -130,6 +133,11 @@ export default {
       default: null
       default: null
     }
     }
   },
   },
+  data () {
+    return {
+      inputValue: ''
+    }
+  },
   computed: {
   computed: {
     attrKey () {
     attrKey () {
       return this.attr?.key
       return this.attr?.key
@@ -213,6 +221,16 @@ export default {
       return this.attrOptions.strats ?? []
       return this.attrOptions.strats ?? []
     }
     }
   },
   },
+  watch: {
+    node: {
+      handler () {
+        if (this.isTextArea) {
+          this.inputValue = this.node[this.attrKey]
+        }
+      },
+      immediate: true
+    }
+  },
   methods: {
   methods: {
     changeColor (color) {
     changeColor (color) {
       if (!color) {
       if (!color) {
@@ -230,6 +248,16 @@ export default {
     },
     },
     toFit (strat) {
     toFit (strat) {
       strat.callback?.(this.node, this.root)
       strat.callback?.(this.node, this.root)
+    },
+    onKeyEnter (e) {
+      if (this.isTextArea && this.attrOptions.singleLine) {
+        e.stopPropagation()
+        e.preventDefault()
+        this.onChange()
+      }
+    },
+    onChange () {
+      this.node[this.attrKey] = this.inputValue
     }
     }
   }
   }
 }
 }

+ 68 - 0
src/views/bigscreen/core/config-json/live.js

@@ -0,0 +1,68 @@
+import { layerNameOption, positionConfig } from './base'
+
+export default {
+  type: 'CLive',
+  label: '直播',
+  icon: 'el-icon-video-camera',
+  defaults (scale) {
+    return {
+      top: 0,
+      left: 0,
+      width: 300 * scale.width | 0,
+      height: 200 * scale.height | 0,
+      radius: 0,
+      mute: 1,
+      url: ''
+    }
+  },
+  copy ({ sources, mute, ...widget }) {
+    widget.sources = sources.map(source => {
+      return { ...source }
+    })
+    widget.mute = 1
+    return widget
+  },
+  options: [
+    {
+      label: '配置',
+      list: [
+        layerNameOption,
+        {
+          key: 'radius',
+          label: '圆角',
+          type: 'number'
+        },
+        {
+          key: 'mute',
+          label: '音频输出',
+          type: 'boolean',
+          options: {
+            display (root) {
+              return !root.bgm.length
+            },
+            callback (root) {
+              if (!this.mute) {
+                root.widgets.forEach(widget => {
+                  if (widget.mute === 0 && widget !== this) {
+                    widget.mute = 1
+                  }
+                })
+              }
+            },
+            active: 0,
+            inactive: 1
+          }
+        },
+        {
+          key: 'url',
+          label: '直播地址',
+          type: 'textarea',
+          options: {
+            singleLine: true
+          }
+        }
+      ]
+    },
+    positionConfig
+  ]
+}

+ 4 - 1
src/views/bigscreen/core/config-json/marquee.js

@@ -30,7 +30,10 @@ export default {
         {
         {
           key: 'text',
           key: 'text',
           label: '文本',
           label: '文本',
-          type: 'textarea'
+          type: 'textarea',
+          options: {
+            singleLine: true
+          }
         },
         },
         {
         {
           key: 'color',
           key: 'color',

+ 4 - 1
src/views/bigscreen/core/config-json/web.js

@@ -21,7 +21,10 @@ export default {
         {
         {
           key: 'href',
           key: 'href',
           label: '链接地址',
           label: '链接地址',
-          type: 'text'
+          type: 'textarea',
+          options: {
+            singleLine: true
+          }
         }
         }
       ]
       ]
     },
     },

+ 3 - 1
src/views/bigscreen/core/utils.js

@@ -6,6 +6,7 @@ import widgetVideo from './config-json/video'
 import widgetTime from './config-json/time'
 import widgetTime from './config-json/time'
 import widgetWeather from './config-json/weather'
 import widgetWeather from './config-json/weather'
 import widgetWeb from './config-json/web'
 import widgetWeb from './config-json/web'
+import widgetLive from './config-json/live'
 
 
 export const widgets = Object.freeze([
 export const widgets = Object.freeze([
   widgetText,
   widgetText,
@@ -14,7 +15,8 @@ export const widgets = Object.freeze([
   widgetVideo,
   widgetVideo,
   widgetTime,
   widgetTime,
   widgetWeather,
   widgetWeather,
-  widgetWeb
+  widgetWeb,
+  widgetLive
 ])
 ])
 
 
 let scale = 1
 let scale = 1

+ 142 - 0
src/views/bigscreen/core/widget/CLive.vue

@@ -0,0 +1,142 @@
+<template>
+  <div
+    class="c-live iconfont"
+    :style="styles"
+  >
+    <video
+      ref="player"
+      class="c-live__inst"
+      :muted.prop="muted"
+    />
+    <canvas
+      v-show="isSnapping"
+      ref="canvas"
+      class="c-live__canvas"
+    />
+  </div>
+</template>
+
+<script>
+import HlsJs from 'hls.js'
+
+const isSupported = HlsJs.isSupported()
+
+export default {
+  name: 'CLive',
+  inject: ['control'],
+  props: {
+    node: {
+      type: Object,
+      default: null
+    }
+  },
+  computed: {
+    isSnapping () {
+      return this.control.snapping
+    },
+    styles () {
+      const {
+        width,
+        height,
+        radius
+      } = this.node
+      return {
+        width: `${width}px`,
+        height: `${height}px`,
+        'font-size': `${Math.min(width, height) / 2 | 0}px`,
+        'border-radius': `${radius}px`
+      }
+    },
+    url () {
+      return this.node.url
+    },
+    muted () {
+      return this.node.mute || this.control.muted
+    }
+  },
+  watch: {
+    url (val) {
+      console.log('watch', val)
+      this.play()
+    },
+    isSnapping (val) {
+      const player = this.$refs.player
+      if (player) {
+        if (val) {
+          this.getSnap()
+        } else {
+          const canvas = this.$refs.canvas
+          if (canvas) {
+            canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height)
+          }
+        }
+      }
+    }
+  },
+  mounted () {
+    this.url && this.play()
+  },
+  beforeDestroy () {
+    if (this.hlsjs) {
+      this.hlsjs.destroy()
+    }
+  },
+  methods: {
+    play () {
+      if (isSupported) {
+        if (!this.hlsjs) {
+          this.hlsjs = new HlsJs()
+          this.hlsjs.attachMedia(this.$refs.player)
+          this.hlsjs.on(HlsJs.Events.MANIFEST_PARSED, () => {
+            this.$refs.player.play()
+          })
+          this.hlsjs.on(HlsJs.Events.ERROR, e => {
+            console.log('error', e)
+          })
+        }
+        this.hlsjs.loadSource(this.url)
+      }
+    },
+    getSnap () {
+      const video = this.$refs.player
+      const canvas = this.$refs.canvas
+      if (video && canvas) {
+        const cxt = canvas.getContext('2d')
+        canvas.width = this.node.width
+        canvas.height = this.node.height
+        cxt.drawImage(video, 0, 0, this.node.width, this.node.height)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-live {
+  position: relative;
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  background-color: rgba(255, 255, 255, .8);
+  overflow: hidden;
+
+  &::before {
+    content: '\e772';
+    font-family: element-icons;
+    font-size: inherit;
+  }
+
+  &__inst,
+  &__canvas {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+  }
+
+  &__inst {
+    object-fit: fill;
+  }
+}
+</style>

+ 23 - 5
src/views/bigscreen/core/widget/CWeb.vue

@@ -3,7 +3,13 @@
     class="c-web"
     class="c-web"
     :style="styles"
     :style="styles"
   >
   >
-    <i class="c-web__icon iconfont iconchaolianjie" />
+    <iframe
+      class="c-web__iframe"
+      :src="node.href"
+    />
+    <i
+      class="c-web__icon iconfont iconchaolianjie"
+    />
   </div>
   </div>
 </template>
 </template>
 
 
@@ -35,14 +41,26 @@ export default {
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 .c-web {
 .c-web {
   position: relative;
   position: relative;
-  display: inline-flex;
-  justify-content: center;
-  align-items: center;
-  white-space: pre-wrap;
+  display: inline-block;
   background-color: rgba(255, 255, 255, .8);
   background-color: rgba(255, 255, 255, .8);
 
 
+  &__iframe {
+    width: 100%;
+    height: 100%;
+    border: none;
+  }
+
   &__icon {
   &__icon {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
     font-size: inherit;
     font-size: inherit;
+    z-index: 9;
   }
   }
 }
 }
 </style>
 </style>

+ 0 - 70
src/views/bigscreen/core/widget/CWebViewer.vue

@@ -1,70 +0,0 @@
-<template>
-  <div
-    class="c-web"
-    :style="styles"
-  >
-    <iframe
-      v-if="isValid"
-      class="c-web__iframe"
-      :src="node.href"
-    />
-    <i
-      v-else
-      class="c-web__icon iconfont iconchaolianjie"
-    />
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'CWebViewer',
-  props: {
-    node: {
-      type: Object,
-      default: null
-    }
-  },
-  computed: {
-    styles () {
-      const {
-        width,
-        height
-      } = this.node
-      return {
-        width: `${width}px`,
-        height: `${height}px`,
-        'font-size': this.isValid ? 'initial' : `${Math.min(width, height) / 3 | 0}px`
-      }
-    },
-    isValid () {
-      return !!this.node.href
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.c-web {
-  position: relative;
-
-  &__iframe {
-    width: 100%;
-    height: 100%;
-    border: none;
-  }
-
-  &__icon {
-    font-size: inherit;
-  }
-
-  &::after {
-    content: '';
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    z-index: 9;
-  }
-}
-</style>

+ 3 - 1
src/views/bigscreen/core/widget/Widget.vue

@@ -25,6 +25,7 @@ import CVideo from './CVideo.vue'
 import CTime from './CTime.vue'
 import CTime from './CTime.vue'
 import CWeather from './CWeather.vue'
 import CWeather from './CWeather.vue'
 import CWeb from './CWeb.vue'
 import CWeb from './CWeb.vue'
+import CLive from './CLive.vue'
 
 
 export default {
 export default {
   name: 'Widget',
   name: 'Widget',
@@ -36,7 +37,8 @@ export default {
     CVideo,
     CVideo,
     CTime,
     CTime,
     CWeather,
     CWeather,
-    CWeb
+    CWeb,
+    CLive
   },
   },
   props: {
   props: {
     scale: {
     scale: {

+ 4 - 2
src/views/bigscreen/core/widget/WidgetViewer.vue

@@ -15,7 +15,8 @@ import CImage from './CImage.vue'
 import CVideo from './CVideo.vue'
 import CVideo from './CVideo.vue'
 import CTime from './CTime.vue'
 import CTime from './CTime.vue'
 import CWeather from './CWeather.vue'
 import CWeather from './CWeather.vue'
-import CWeb from './CWebViewer.vue'
+import CWeb from './CWeb.vue'
+import CLive from './CLive.vue'
 
 
 export default {
 export default {
   name: 'Widget',
   name: 'Widget',
@@ -26,7 +27,8 @@ export default {
     CVideo,
     CVideo,
     CTime,
     CTime,
     CWeather,
     CWeather,
-    CWeb
+    CWeb,
+    CLive
   },
   },
   props: {
   props: {
     node: {
     node: {

+ 5 - 0
yarn.lock

@@ -5253,6 +5253,11 @@ highlight.js@^10.7.1:
   resolved "https://registry.npmmirror.com/highlight.js/download/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
   resolved "https://registry.npmmirror.com/highlight.js/download/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
   integrity sha1-aXJy45kTVuQMPKxWanTu9oF1ZTE=
   integrity sha1-aXJy45kTVuQMPKxWanTu9oF1ZTE=
 
 
+hls.js@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.1.2.tgz#71691e11928e54ead9696bc64b88c1b9914c394d"
+  integrity sha512-ujditC4vvBmZd00RRNfNPLgFVlqEeUX4sAFv5lGhBHuql8iAZodOdlZTD3em/1zo7vyjQp12up/lCVqQk8dvxA==
+
 hmac-drbg@^1.0.1:
 hmac-drbg@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.nlark.com/hmac-drbg/download/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
   resolved "https://registry.nlark.com/hmac-drbg/download/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"