vue-element-admin 集成阿里云VOD点播

目前负责的一个系统里的点播要使用阿里云的VOD点播,经过大概研读阿里云的帮助文档,并且做了一些实质性的开发。现将开发思路记录下来,以免以后又掉进坑里。

第一步,下载阿里云VOD的JAVASCRIPT版本的SDK。

第二步,将下载下来的阿里云VOD的JAVASCRIPT版本的SDK放在VUE-ELEMENT-ADMIN项目目录/public/

第三步,在VUE-ELEMENT-ADMIN项目文件/public/index.html的head部分新增以下代码

<script src="<%= BASE_URL %>aliyun-upload-sdk-1.5.2/lib/es6-promise.min.js"></script>
<script src="<%= BASE_URL %>aliyun-upload-sdk-1.5.2/lib/aliyun-oss-sdk-6.13.0.min.js"></script>
<script src="<%= BASE_URL %>aliyun-upload-sdk-1.5.2/aliyun-upload-sdk-1.5.2.min.js"></script>

第四步,新增showVodUploadPanel状态,用于打开视频上传组件面板。修改/src/store/modules/app.js

const state = {
  sidebar: {
    opened: window.localStorage.getItem('sidebarStatus') ? !!+window.localStorage.getItem('sidebarStatus') : false,
    withoutAnimation: false
  },
  device: 'desktop',
  showVodUploadPanel: false
}

const mutations = {
  TOGGLE_SIDEBAR: state => {
    state.sidebar.opened = !state.sidebar.opened
    state.sidebar.withoutAnimation = false
    if (state.sidebar.opened) {
      window.localStorage.setItem('sidebarStatus', 1)
    } else {
      window.localStorage.setItem('sidebarStatus', 0)
    }
  },
  CLOSE_SIDEBAR: (state, withoutAnimation) => {
    window.localStorage.setItem('sidebarStatus', 0)
    state.sidebar.opened = false
    state.sidebar.withoutAnimation = withoutAnimation
  },
  TOGGLE_DEVICE: (state, device) => {
    state.device = device
  },
  OPEN_VODUPLOADPANEL: state => {
    state.showVodUploadPanel = true
  },
  CLOSE_VODUPLOADPANEL: state => {
    state.showVodUploadPanel = false
  }
}

const actions = {
  toggleSideBar({ commit }) {
    commit('TOGGLE_SIDEBAR')
  },
  closeSideBar({ commit }, { withoutAnimation }) {
    commit('CLOSE_SIDEBAR', withoutAnimation)
  },
  toggleDevice({ commit }, device) {
    commit('TOGGLE_DEVICE', device)
  },
  openVodUploadPanel({ commit }) {
    commit('OPEN_VODUPLOADPANEL')
  },
  closeVodUploadPanel({ commit }) {
    commit('CLOSE_VODUPLOADPANEL')
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

第五步,修改/src/store/getters.js

const getters = {
  // ..................................多余部分省略
  showVodUploadPanel: state => state.app.showVodUploadPanel
}
export default getters

第六步,在导航栏右侧新增打开视频上传面板组件的按钮,修改文件/src/layout/components/Navbar.vue

<template>
  <div class="navbar">
    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />

    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />

    <div class="right-menu">
      <template v-if="device!=='mobile'">
        <div class="right-menu-item hover-effect">
          <i class=" el-icon-film" @click="openVodUploadPanel" />
        </div>

        <search id="header-search" class="right-menu-item" />
    openVodUploadPanel() {
      this.$store.dispatch('app/openVodUploadPanel')
    }

由于视频文件一般都比较大,为了在上传视频的时候不影响其他操作,我选择在布局文件上添加视频上传按钮。

第七步,新增视频上传组件。创建文件/src/layout/components/MediaUpload/index.vue

<template>
  <div class="media-upload-container">
    <div class="upload-info" @click="handlerVodFileSelect">
      <div class="upload-info-icon">
        <i class="el-icon-upload" />
        <div class="upload-info-icon-help">
          点击这里上传
        </div>
      </div>
      <div class="upload-info-text">
        支持3GP、ASF、AVI、DAT、DV、FLV、F4V、GIF、M2T、M4V、MJ2、MJPEG、MKV、MOV、MP4、MPE、MPG、MPEG、MTS、OGG、QT、RM、RMVB、SWF、TS、VOB、WMV、WEBM 等视频格式上传 , 音频支持aac, ac3, acm, amr, ape, caf, flac, m4a, mp3, ra, wav, wma
      </div>
      <input ref="vodFileInput" type="file" multiple class="upload-info-btn" @change="fileChange">
    </div>
    <div v-if="showVodFileInfo">
      <el-table

        :data="vodFileInfo"
        :header-cell-style="{'background':'#f4f4f4'}"
        style="width: 100%"
        class="vod-file-info-table"
      >
        <el-table-column
          prop="name"
          label="音/视频名称"
        >
          <template slot-scope="scope">
            <el-input v-model="scope.row.name" placeholder="请输入标题" />
          </template>
        </el-table-column>
        <el-table-column
          prop="type"
          label="格式"
          width="120"
        />
        <el-table-column
          prop="size"
          label="大小"
          width="120"
        >
          <template slot-scope="scope">
            <span v-text="handleFormatFileSize(scope.row.size)" />
          </template>
        </el-table-column>
      </el-table>
      <el-row>
        <el-col :span="20">
          <el-progress :text-inside="true" :stroke-width="28" :percentage="authProgress" :format="uploadStatusFormat" :class="{'progress-ready': progressReady}" />
        </el-col>
        <el-col :span="4" style="background-color: #e6ebf5; text-align: right">
          <el-button :disabled="uploadDisabled" type="primary" size="mini" @click="authUpload">开始上传</el-button>
          <el-button :disabled="uploadDisabled" type="primary" size="mini" @click="resumeUpload">恢复上传</el-button>
        </el-col>
      </el-row>

    </div>

    <el-table
      v-if="showVodUploadSuccessInfo"
      :data="VodUploadSuccessInfo"
      :header-cell-style="{'background':'#f4f4f4'}"
      style="width: 100%"
      class="vod-upload-success-info-table"
    >
      <el-table-column
        prop="file.name"
        label="音/视频名称"
      />
      <el-table-column
        prop="file.type"
        label="格式"
      />
      <el-table-column
        prop="file.size"
        label="大小"
      >
        <template slot-scope="scope">
          <span v-text="handleFormatFileSize(scope.row.file.size)" />
        </template>
      </el-table-column>
      <el-table-column
        prop="state"
        label="上传状态"
      />
      <el-table-column
        label="操作"
        align="right"
      >
        <template slot-scope="scope">
          <el-button type="text" @click="deleteVod(scope.row.videoId)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
import axios from 'axios'
import { getOptionsByCurrentServiceId } from '@/api/options'
import { formatBytesSize } from '@/utils/index'
export default {
  data() {
    return {
      loading: true,
      aliyun_vod_accessKey_id: '',
      aliyun_vod_accessKey_secret: '',
      aliyun_vod_timeout: 60000,
      aliyun_vod_partSize: 1048576,
      aliyun_vod_parallel: 5,
      aliyun_vod_retryCount: 3,
      aliyun_vod_retryDuration: 2,
      aliyun_vod_region: 'cn-shanghai',
      aliyun_vod_userId: '1303984639806000',
      saveLoading: false,
      uploader: null,
      statusText: '',
      authProgress: 0,
      uploadDisabled: true,
      resumeDisabled: true,
      pauseDisabled: true,
      showVodFileInfo: false,
      vodFileInfo: [],
      showVodUploadSuccessInfo: false,
      VodUploadSuccessInfo: [],
      progressReady: false
    }
  },
  created() {
    this.getConfig()
  },
  methods: {
    handleFormatFileSize(num) {
      return formatBytesSize(num)
    },
    handlerVodFileSelect() {
      this.$refs.vodFileInput.dispatchEvent(new MouseEvent('click'))
    },
    fileChange() {
      const files = this.$refs.vodFileInput.files
      for (let index = 0; index < files.length; index++) {
        const file = files[index]
        if (!file) {
          this.$message({
            message: '请选择需要上传的文件',
            type: 'warning'
          })
        } else {
          if (this.uploader) {
            this.uploader.stopUpload()
            this.authProgress = 0
            this.statusText = ''
          } else {
            this.uploader = this.createUploader()
          }
          const userData = '{"Vod":{}}'
          this.uploader.addFile(file, null, null, null, userData)
          this.uploadDisabled = false
          this.pauseDisabled = true
          this.resumeDisabled = false
          this.vodFileInfo.unshift(file)
        }
      }
      this.progressReady = true
      this.showVodFileInfo = true
    },
    // 开始上传
    authUpload() {
      // 然后调用 startUpload 方法, 开始上传
      if (this.uploader !== null) {
        this.uploader.startUpload()
        this.uploadDisabled = true
        this.pauseDisabled = false
      }
    },
    // 暂停上传
    pauseUpload() {
      if (this.uploader !== null) {
        this.uploader.stopUpload()
        this.resumeDisabled = false
        this.pauseDisabled = true
      }
    },
    // 恢复上传
    resumeUpload() {
      if (this.uploader !== null) {
        this.uploader.startUpload()
        this.resumeDisabled = true
        this.pauseDisabled = false
      }
    },
    uploadStatusFormat(percentage) {
      return percentage > 0 ? (this.statusText + percentage + '%') : this.statusText
    },
    createUploader() {
      const self = this
      const uploader = new AliyunUpload.Vod({
        timeout: self.aliyun_vod_timeout || 60000,
        partSize: self.aliyun_vod_partSize || 1048576,
        parallel: self.aliyun_vod_parallel || 5,
        retryCount: self.aliyun_vod_retryCount || 3,
        retryDuration: self.aliyun_vod_retryDuration || 2,
        region: self.aliyun_vod_region,
        userId: self.aliyun_vod_userId,
        // 添加文件成功
        addFileSuccess: function(uploadInfo) {
          self.uploadDisabled = false
          self.resumeDisabled = false
          self.statusText = '添加文件成功, 等待上传...'
          console.log('addFileSuccess: ' + uploadInfo.file.name)
        },
        // 开始上传
        onUploadstarted: function(uploadInfo) {
          // 如果是 UploadAuth 上传方式, 需要调用 uploader.setUploadAuthAndAddress 方法
          // 如果是 UploadAuth 上传方式, 需要根据 uploadInfo.videoId是否有值,调用点播的不同接口获取uploadauth和uploadAddress
          // 如果 uploadInfo.videoId 有值,调用刷新视频上传凭证接口,否则调用创建视频上传凭证接口
          // 注意: 这里是测试 demo 所以直接调用了获取 UploadAuth 的测试接口, 用户在使用时需要判断 uploadInfo.videoId 存在与否从而调用 openApi
          // 如果 uploadInfo.videoId 存在, 调用 刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)
          // 如果 uploadInfo.videoId 不存在,调用 获取视频上传地址和凭证接口(https://help.aliyun.com/document_detail/55407.html)
          if (!uploadInfo.videoId) {
            const createUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/CreateUploadVideo?Title=testvod1&FileName=aa.mp4&BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&VideoId=5bfcc7864fc14b96972842172207c9e6'
            axios.get(createUrl).then(({ data }) => {
              const uploadAuth = data.UploadAuth
              const uploadAddress = data.UploadAddress
              const videoId = data.VideoId
              uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId)
            })
            self.statusText = '文件开始上传...'
            console.log('onUploadStarted:' + uploadInfo.file.name + ', endpoint:' + uploadInfo.endpoint + ', bucket:' + uploadInfo.bucket + ', object:' + uploadInfo.object)
          } else {
            // 如果videoId有值,根据videoId刷新上传凭证
            // https://help.aliyun.com/document_detail/55408.html?spm=a2c4g.11186623.6.630.BoYYcY
            const refreshUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/RefreshUploadVideo?BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&Title=haha1&FileName=xxx.mp4&VideoId=' + uploadInfo.videoId
            axios.get(refreshUrl).then(({ data }) => {
              const uploadAuth = data.UploadAuth
              const uploadAddress = data.UploadAddress
              const videoId = data.VideoId
              uploader.setUploadAuthAndAddress(uploadInfo, uploadAuth, uploadAddress, videoId)
            })
          }
        },
        // 文件上传成功
        onUploadSucceed: function(uploadInfo) {
          console.log('onUploadSucceed: ' + uploadInfo.file.name + ', endpoint:' + uploadInfo.endpoint + ', bucket:' + uploadInfo.bucket + ', object:' + uploadInfo.object)
          self.statusText = '文件上传成功!'
          self.VodUploadSuccessInfo.unshift(uploadInfo)
          self.vodFileInfo.forEach((item, index) => {
            if (item.name === uploadInfo.file.name) {
              self.$delete(self.vodFileInfo, index)
            }
          })
          self.showVodUploadSuccessInfo = true
          self.createVod(uploadInfo)
        },
        // 文件上传失败
        onUploadFailed: function(uploadInfo, code, message) {
          console.log('onUploadFailed: file:' + uploadInfo.file.name + ',code:' + code + ', message:' + message)
          self.statusText = '文件上传失败!'
        },
        // 取消文件上传
        onUploadCanceled: function(uploadInfo, code, message) {
          console.log('Canceled file: ' + uploadInfo.file.name + ', code: ' + code + ', message:' + message)
          self.statusText = '文件已暂停上传'
        },
        // 文件上传进度,单位:字节, 可以在这个函数中拿到上传进度并显示在页面上
        onUploadProgress: function(uploadInfo, totalSize, progress) {
          const progressPercent = Number((progress * 100).toFixed(2))
          self.authProgress = progressPercent
          self.statusText = uploadInfo.file.name + ' 上传中...'
          if (self.progressReady === true && progress > 0) {
            self.progressReady = false
          }
          console.log('onUploadProgress:file:' + uploadInfo.file.name + ', fileSize:' + totalSize + ', percent:' + progressPercent + '%')
        },
        // 上传凭证超时
        onUploadTokenExpired: function(uploadInfo) {
          // 上传大文件超时, 如果是上传方式一即根据 UploadAuth 上传时
          // 需要根据 uploadInfo.videoId 调用刷新视频上传凭证接口(https://help.aliyun.com/document_detail/55408.html)重新获取 UploadAuth
          // 然后调用 resumeUploadWithAuth 方法, 这里是测试接口, 所以我直接获取了 UploadAuth
          const refreshUrl = 'https://demo-vod.cn-shanghai.aliyuncs.com/voddemo/RefreshUploadVideo?BusinessType=vodai&TerminalType=pc&DeviceModel=iPhone9,2&UUID=59ECA-4193-4695-94DD-7E1247288&AppVersion=1.0.0&Title=haha1&FileName=xxx.mp4&VideoId=' + uploadInfo.videoId
          axios.get(refreshUrl).then(({ data }) => {
            const uploadAuth = data.UploadAuth
            uploader.resumeUploadWithAuth(uploadAuth)
            console.log('upload expired and resume upload with uploadauth ' + uploadAuth)
          })
          self.statusText = '文件超时...'
        },
        // 全部文件上传结束
        onUploadEnd: function(uploadInfo) {
          self.statusText = '文件上传完毕'
          self.showVodFileInfo = false
        }
      })
      return uploader
    },
    getConfig() {
      this.loading = true
      getOptionsByCurrentServiceId().then(response => {
        if (response.data != null) {
          this.aliyun_vod_accessKey_id = response.data.aliyun_vod_accessKey_id
          this.aliyun_vod_accessKey_secret = response.data.aliyun_vod_accessKey_secret
          this.aliyun_vod_timeout = response.data.aliyun_vod_timeout
          this.aliyun_vod_partSize = response.data.aliyun_vod_partSize
          this.aliyun_vod_parallel = response.data.aliyun_vod_parallel
          this.aliyun_vod_retryCount = response.data.aliyun_vod_retryCount
          this.aliyun_vod_retryDuration = response.data.aliyun_vod_retryDuration
          this.aliyun_vod_region = response.data.aliyun_vod_region
          this.aliyun_vod_userId = response.data.aliyun_vod_userId
        }
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },
    deleteVod(videoId) {

    },
    createVod(videoId) {

    }
  }
}
</script>

<style lang="scss" scoped>
.media-upload-container {
  position: relative;
  padding: 20px;
  .upload-info {
    padding: 15px 0;
    color: #c1c1c1;
    text-align: center;
    border: 1px dashed #c1c1c1;
    cursor: pointer;
    .upload-info-icon {
      color: #111;
      margin-bottom: 10px;
      .el-icon-upload {
        font-size: 48px;
      }
      .upload-info-icon-help {
        font-size: 16px;
      }
    }
    .upload-info-text {
      font-size: 14px;
    }
    .upload-info-btn {
      display: none;
    }
  }
  .vod-file-info-table {
    margin: 20px 0;
  }
  .vod-upload-success-info-table {
    margin: 20px 0 0 0;
  }
  .el-progress {
    &.progress-ready {
    position: relative;
      /deep/.el-progress-bar__innerText {
        color: #1682e6;
      }
    }
    /deep/.el-progress-bar__outer {
      border-radius: 0;
      .el-progress-bar__inner {
        border-radius: 0;
      }
    }
  }
}
</style>

第八步,修改/src/layout/index.vue

<template>
  <div :class="classObj" class="app-wrapper">
    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
    <sidebar class="sidebar-container" />
    <div :class="{hasTagsView:needTagsView}" class="main-container">
      <div :class="{'fixed-header':fixedHeader}">
        <navbar />
        <tags-view v-if="needTagsView" />
      </div>
      <app-main :class="workbenchSidebarSatausObj" />
      <workbench-sidebar />
      <right-panel v-if="showSettings">
        <settings />
      </right-panel>
      <el-drawer
        title="视频上传"
        size="1200px"
        :visible="showPanel"
        @open="openDrawer"
        @close="closeDrawer"
      >
        <media-upload />
      </el-drawer>
    </div>
  </div>
</template>

<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, WorkbenchSidebar, TagsView, MediaUpload } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'

export default {
  name: 'Layout',
  components: {
    AppMain,
    Navbar,
    RightPanel,
    Settings,
    Sidebar,
    WorkbenchSidebar,
    TagsView,
    MediaUpload
  },
  mixins: [ResizeMixin],
  data() {
    return {
      showPanel: false
    }
  },
  computed: {
    ...mapState({
      sidebar: state => state.app.sidebar,
      workbenchSidebar: state => state.app.workbenchSidebar,
      device: state => state.app.device,
      showSettings: state => state.settings.showSettings,
      needTagsView: state => state.settings.tagsView,
      fixedHeader: state => state.settings.fixedHeader
    }),
    classObj() {
      return {
        hideSidebar: !this.sidebar.opened,
        openSidebar: this.sidebar.opened,
        withoutAnimation: this.sidebar.withoutAnimation,
        mobile: this.device === 'mobile'
      }
    },
    workbenchSidebarSatausObj() {
      return {
        hideWorkbenchSidebar: !this.workbenchSidebar.opened,
        openWorkbenchSidebar: this.workbenchSidebar.opened
      }
    }
  },
  watch: {
    '$store.state.app.showVodUploadPanel'(val) {
      this.showPanel = val
    }
  },
  methods: {
    handleClickOutside() {
      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
    },
    openDrawer() {
      this.$store.dispatch('app/openVodUploadPanel')
    },
    closeDrawer() {
      this.$store.dispatch('app/closeVodUploadPanel')
    }
  }
}
</script>

<style lang="scss" scoped>
  @import "~@/styles/mixin.scss";
  @import "~@/styles/variables.scss";

  .app-wrapper {
    @include clearfix;
    position: relative;
    height: 100%;
    width: 100%;
    &.mobile.openSidebar {
      position: fixed;
      top: 0;
    }
  }

  .drawer-bg {
    background: #000;
    opacity: 0.3;
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
  }

  .fixed-header {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 9;
    width: calc(100% - #{$sideBarWidth});
    transition: width 0.28s;
    &.driver-fix-stacking {
      position: absolute;
    }
  }

  .hideSidebar .fixed-header {
    width: 100%
  }

  .mobile .fixed-header {
    width: 100%;
  }
</style>

最后来一张最终效果图