vue-core-video-player
视频组件:
<template>
<div>
<el-dialog :visible.sync="isShow" title="设置封面" :close-on-click-modal="false" width="750px" class="dialog-dfl" :before-close="beforeClose">
<div>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="封面截取" name="one">
<div class=" nochose" v-if="!file">
暂不支持预览选帧
<div>重新上传本地视频,再截选一帧;或直接<span @click="uploadimg">上传图片</span></div>
</div>
<div class="conts" v-else style="height: 403px" v-loading="loading" element-loading-background="#fff" element-loading-text="解析中">
<div v-show="!loading">
<div class="look_img">
<img :src="cover.ossImgPath + imgForm.ossid" alt="" :key="imgForm.ossid" />
</div>
<div class="imgs_list_box">
<div class="imgs_list">
<div v-for="(item, index) in imgForm.img_list" :key="index" class="imgs_item">
<img :src="item" alt="" />
</div>
<div class="slider-dfl">
<span style="color:transparent">{{ imgForm.videoTime }} {{ sliderVal }}</span>
<el-slider v-model="sliderVal" :step="0.01" :min="0" :max="imgForm.videoTime" placement="bottom" :format-tooltip="formatTooltip" @change="sliderChange" />
</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="本地上传" name="two">
<div v-loading="false" element-loading-background="#fff" element-loading-text="解析中" style="height: 403px" class="cover-upload">
<div v-if="true" class="conts_right">
<img class="detailBg" :src="cover.ossImgPath + coverImg.ossid" alt="" v-if="coverImg.ossid" />
<el-upload :action="cover.coverUploadUrl" class="upload-img" :headers="{ Authorization: `Bearer ${$store.getters.token}` }" :show-file-list="false" :data="cover.uploadData" name="files" ref="coverImg" key="coverImg" :before-upload="beforeAvatarUpload" :on-success="(response, file, fileList) => handlecoverAvatarSuccess(response, file, fileList)">
<div class="noimg-upload" v-if="!coverImg.ossid">
<img src="@/assets/img/upload.png" alt="" class="upload-icon" />
<el-button class="fot-btn upload-btn">
上传封面
</el-button>
</div>
<div v-else class="re-upload"><img src="@/assets/img/reupload.png" alt="" class="reupload-icon" />重新上传</div>
</el-upload>
<!-- <div class="el-upload__tip">图片上传尺寸建议370*370,文件小于400K,仅支持JPG,PNG</div> -->
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="calcle-btn fot-btn" @click="beforeClose">
取消
</el-button>
<el-button class="submit-btn fot-btn" @click="confirmCover">
确认
</el-button>
<!-- <div v-show="activeName == 'two'" class="bottom_btn">
<div class="r">
<el-button color="#fe3355" @click="confirmCoverTwo">
确认封面
</el-button>
</div>
</div> -->
</div>
</template>
</el-dialog>
</div>
</template>
<script>
// import { VueCropper } from 'vue-cropper'
import mixins from './getossid'
export default {
// components: { VueCropper },
props: {
isShow: {
type: Boolean,
default: () => true
}
//传过来的视频文件
// file: {
// type: Object,
// default: () => ({})
// }
},
mixins: [mixins],
data() {
return {
loading: true,
file: null,
activeName: 'one',
sliderVal: 0,
sliderVal2: 20,
cropperRef: {},
coverImg: {}, //上传封面
imgForm: {
url: '', //封面预览地址
urlTwo: '', //封面预览地址
blob: {}, //封面blob对象
img_list: [], //底部预览条图片数组
videoTime: 0, //视频时长
ossid: '',
oldVideoFile: {} //旧的视频文件
},
cover: {
coverUploadUrl: process.env.BASE_API + '/ykb-oss/oss/uploadFdfs',
ossImgPath: process.env.BASE_API + '/ykb-oss/oss/view?ossId=',
uploadData: {
param: JSON.stringify({
appid: 'oss7wqc3k',
appsecret: 'ok601gkg',
project: 'lms',
opDocType: ''
})
}
},
option: {
img: '',
outputSize: 1, // 裁剪生成图片的质量
outputType: 'jpeg', // 裁剪生成图片的格式 jpeg, png, webp
info: true, // 裁剪框的大小信息
canScale: true, // 图片是否允许滚轮缩放
autoCrop: true, // 是否默认生成截图框
autoCropWidth: 200, // 默认生成截图框宽度
autoCropHeight: 200, // 默认生成截图框高度
fixedBox: false, // 固定截图框大小 不允许改变
fixed: false, // 是否开启截图框宽高固定比例
fixedNumber: [1, 1], // 截图框的宽高比例 [ 宽度 , 高度 ]
canMove: true, // 上传图片是否可以移动
canMoveBox: true, // 截图框能否拖动
original: false, // 上传图片按照原始比例渲染
centerBox: false, // 截图框是否被限制在图片里面
infoTrue: true, // true 为展示真实输出图片宽高 false 展示看到的截图框宽高
full: false, // 是否输出原图比例的截图
enlarge: '1', // 图片根据截图框输出比例倍数
mode: 'contain' // 图片默认渲染方式 contain , cover, 100px, 100% auto,
},
duration: '',
url: '',
cropperForm: {
imgLookUrl: '', //裁剪实时预览
bgColor: '#fff', //裁剪图片底色
loadings: true
}
}
},
watch: {
file(newVal, oldVal) {
// if (this.isShow == true && this.file.type) {
// if (this.file.type.includes('video')) {
// //通过验证
// if (newVal.name != this.imgForm.oldVideoFile.name && newVal.size != this.imgForm.oldVideoFile.size) {
// //是否已经选择了这个视频文件 选择了相同的文件就不用初始化了 如果不同就初始化
// this.loading = true
// this.cropperForm.loadings = true
// this.activeName = 'one'
// this.init()
// }
// } else {
// //未通过验证
// this.$message({
// message: '请选择视频格式的文件',
// grouping: true,
// type: 'error'
// })
// this.$emit('closeDialog', false)
// }
// } else if (this.isShow == true) {
// //未通过验证
// this.$message({
// message: '请选择视频格式的文件',
// grouping: true,
// type: 'error'
// })
// this.$emit('closeDialog', false)
// }
},
isShow(newVal, oldVal) {
this.sliderVal = 0.1
this.coverImg = {}
console.log(this.file)
// this.sliderChange(0.1)
// if (this.isShow == true && this.file.type) {
// if (this.file.type.includes('video')) {
// //通过验证
// if (newVal[0].name != this.imgForm.oldVideoFile.name && newVal[0].size != this.imgForm.oldVideoFile.size) {
// //是否已经选择了这个视频文件 选择了相同的文件就不用初始化了 如果不同就初始化
// this.loading = true
// this.cropperForm.loadings = true
// this.activeName = 'one'
// this.init()
// }
// } else {
// //未通过验证
// this.$message({
// message: '请选择视频格式的文件',
// grouping: true,
// type: 'error'
// })
// this.$emit('closeDialog', false)
// }
// } else if (this.isShow == true) {
// //未通过验证
// this.$message({
// message: '请选择视频格式的文件',
// grouping: true,
// type: 'error'
// })
// this.$emit('closeDialog', false)
// }
}
},
created() {},
methods: {
uploadimg() {
this.activeName = 'two'
this.$refs['coverImg'].$refs['upload-inner'].handleClick()
},
changeVideo() {
//关闭弹窗并且触发上传本地视频
this.$emit('closeDialog', false)
},
changeFile(newVal, duration, url) {
this.file = newVal
this.duration = duration
this.url = url
// {
// uid: 1698891870169,
// lastModified: 1692350189838,
// name: 'b.mp4',
// size: 15250255,
// type: 'video/mp4',
// webkitRelativePath: ''
// }
console.log('changeFile', newVal, duration, url)
if (this.file.type) {
if (this.file.type.includes('video')) {
//通过验证
if (newVal.name != this.imgForm.oldVideoFile.name && newVal.size != this.imgForm.oldVideoFile.size) {
//是否已经选择了这个视频文件 选择了相同的文件就不用初始化了 如果不同就初始化
this.loading = true
this.cropperForm.loadings = true
this.activeName = 'one'
this.init()
}
} else {
//未通过验证
this.$message({
message: '请选择视频格式的文件',
grouping: true,
type: 'error'
})
this.$emit('closeDialog', false)
}
} else if (this.isShow == true) {
//未通过验证
this.$message({
message: '请选择视频格式的文件',
grouping: true,
type: 'error'
})
this.$emit('closeDialog', false)
}
},
init() {
this.imgForm.url = ''
this.imgForm.ossid = ''
this.imgForm.blob = {}
this.imgForm.img_list = []
this.imgForm.videoTime = 0
this.imgForm.oldVideoFile = this.file
let reader = new FileReader()
let that = this
//获取视频时长
reader.onload = function(e) {
let video = document.createElement('video')
// @ts-ignore
video.src = e.target.result || that.url
video.addEventListener('loadedmetadata', async function() {
// 这里先看900宽度能放几张图片
const img_src = await that.captureFrame(that.file, Math.floor(video.duration || that.duration))
var img_load = document.createElement('img')
img_load.setAttribute('src', img_src.url)
img_load.onload = function() {
var aspectRatio = img_load.naturalWidth / img_load.naturalHeight
// option.fixedNumber[0] =
// parseFloat((img_load.width / img_load.height).toFixed(2)) - 0.2;
that.option.fixedNumber[0] = img_load.width / img_load.height
var width = 50 * aspectRatio
let count = Math.floor(540 / width) // 总宽度为960 看能放几张图片
let duration = Math.floor(video.duration) //取整
that.imgForm.videoTime = duration
var step = Math.floor(duration / (count - 1)) // 步长
var result = [] // 存储结果的数组
for (var i = 0; i < count; i++) {
result.push(i * step)
}
if (result[0] == 0) {
result[0] = 0.1
}
result.forEach(async (item, index) => {
const res = await that.captureFrame(that.file, item)
if (index == 0) {
that.imgForm.url = res.url
that.getImageBase64(res.blob).then(res => {
that.getossid(res).then(resid => {
that.imgForm.ossid = resid
})
})
that.imgForm.blob = res.blob
}
that.imgForm.img_list.push(res.url)
})
that.sliderChange(0.1)
that.$nextTick(() => {
setTimeout(() => {
that.loading = false
}, 2000)
})
}
})
}
// @ts-ignore
reader.readAsDataURL(this.file)
},
//滑块位置改变 更滑上方主封面图
async sliderChange(val) {
console.log(val, '-=--===9999')
let that = this
const res = await this.captureFrame(this.file, val)
this.imgForm.url = res.url
that.getImageBase64(res.blob).then(res1 => {
that.getossid(res1).then(resid => {
that.$set(that.imgForm, 'ossid', resid)
})
})
this.imgForm.blob = res.blob
console.log(this.imgForm)
},
// 格式化提示时间
formatTooltip(val) {
var timeString = this.convertSeconds(val)
return timeString
},
//关闭模态弹窗
beforeClose() {
this.$emit('closeDialog', false)
},
//确认封面选择封面
confirmCover() {
this.$emit('closeDialog', false)
if (this.activeName == 'one') {
this.$emit('confirmImg', {
url: this.imgForm.url,
blob: this.imgForm.blob,
ossid: this.imgForm.ossid
})
} else {
this.$emit('confirmImg', {
url: this.cover.ossImgPath + this.coverImg.ossid,
ossid: this.coverImg.ossid
// blob: this.imgForm.blob
})
}
},
// 获取视频帧的封面
captureFrame(videoFile, time = 0) {
return new Promise(succeed => {
const video = document.createElement('video')
video.currentTime = time
video.muted = true
video.autoplay = true
video.oncanplay = async () => {
const res = await this.drawVideo(video)
succeed(res)
}
video.src = URL.createObjectURL(videoFile)
})
},
// 画视频
drawVideo(video) {
return new Promise(res => {
const cvs = document.createElement('canvas')
const ctx = cvs.getContext('2d')
cvs.width = video.videoWidth
cvs.height = video.videoHeight
ctx.drawImage(video, 0, 0, cvs.width, cvs.height)
cvs.toBlob(blob => {
res({
blob,
url: URL.createObjectURL(blob)
})
})
})
},
// 秒数换算时间
convertSeconds(seconds) {
var hours = parseInt(seconds / 3600)
var minutes = parseInt(seconds / 60)
var remainingSeconds = parseInt(seconds % 60) //秒
var millisecond = Math.floor((seconds % 60) * 10) //毫秒
var timeString = ''
if (hours > 0) {
timeString += hours + ':'
}
timeString += minutes + ':' + remainingSeconds
return timeString
},
//封面上传
beforeAvatarUpload(file) {
const suffix = file.type === 'image/jpg' || file.type === 'image/png' || file.type === 'image/jpeg'
const isLt1M = file.size / 1024 / 1024 < 1
if (!suffix) {
this.$message.error('只能上传图片!')
return false
}
if (!isLt1M) {
this.$message.error('上传图片大小不能超过 1MB!')
return false
}
},
//封面上传
handlecoverAvatarSuccess(response, file, fileList, index, scope) {
// console.log(this.$refs['coverImg' + index].uploadFiles, response, file, fileList)
let data = {
id: file.id || null,
fileName: file.name,
ossid: response.data[0].id,
fileSize: file.size,
fileExt: file.ext,
type: 6,
duration: file.duration || 0
}
this.$set(this, 'coverImg', data)
},
//本地封面确定事件
confirmCoverTwo() {
// @ts-ignore
this.$refs.cropperRef.getCropData(data => {
const image = new Image()
image.src = data
image.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = image.width
canvas.height = image.height
ctx.drawImage(image, 0, 0, canvas.width, canvas.height)
canvas.toBlob(blob => {
this.$emit('closeDialog', false)
this.$emit('confirmImg', {
url: URL.createObjectURL(blob),
blob
})
})
}
})
}
}
}
</script>
<style lang="scss" scoped>
.conts {
height: 403px;
box-sizing: border-box;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: center;
.look_img {
display: flex;
justify-content: center;
margin-bottom: 25px;
img {
width: 540px !important;
height: 309px !important;
border-radius: 4px;
}
}
.imgs_list_box {
display: flex;
align-items: center;
justify-content: center;
}
.imgs_list {
position: relative;
width: auto !important;
height: 91px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: center;
.imgs_item {
img {
width: auto !important;
height: 60px !important;
}
}
}
}
.nochose {
text-align: center;
height: 403px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
div {
margin-top: 10px;
}
span {
color: #00aeec;
cursor: pointer;
}
}
.slider-dfl {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.cover-upload {
display: flex;
// align-items: center;
justify-content: center;
}
.conts_right {
height: 309px;
width: 540px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
background: #f7f7f7ff;
border: 1px dashed #d9d9d9;
position: relative;
.l {
flex: 1;
height: 100%;
border: 1px solid #e4e7ed;
margin-right: 20px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.r {
flex: 1;
height: 100%;
border: 1px solid #e4e7ed;
}
.upload-icon {
width: 40px;
height: 40px;
margin-bottom: 24px;
}
.noimg-upload {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.upload-btn {
background: #f5b051ff;
color: #ffffffff;
border: 1px solid #f5b051ff;
&:hover {
opacity: 0.7;
}
}
.detailBg {
width: 100%;
height: 100%;
}
.re-upload {
position: absolute;
bottom: -30px;
width: 200px;
color: #f03e40ff;
text-align: left;
right: calc(50% - 20px);
left: calc(50% - 20px);
.reupload-icon {
width: 13px;
height: 13px;
position: relative;
top: 1px;
margin-right: 4px;
}
}
}
.bottom_btn {
display: flex;
align-items: center;
justify-content: space-between;
.l {
display: flex;
align-items: center;
}
}
#img-file {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.dialog-footer {
text-align: center;
}
.fot-btn {
width: 110px;
}
.calcle-btn {
color: #848a90ff;
border: 1px solid #dcdfe6ff;
&:hover {
opacity: 0.7;
background-color: #f5f7faff;
}
}
.submit-btn {
color: #ffffffff;
background: #f03e40ff;
border: 1px solid #f03e40ff;
&:hover {
opacity: 0.7;
}
}
/deep/ .el-dialog__headerbtn .el-dialog__close {
color: #1c1f21ff;
font-weight: 600;
}
/deep/ button.el-dialog__headerbtn {
background: #f5f7faff;
padding: 4px 2px 2px 2px;
}
/deep/ span.el-dialog__title {
color: #1c1f21ff;
font-size: 20px;
font-weight: 400;
}
/deep/ .el-dialog__header {
border-bottom: 1px solid #f3f5f6ff;
}
/deep/ .dialog-dfl .el-dialog__body {
padding-top: 24px;
padding-bottom: 0px;
}
/deep/ .dialog-dfl .el-tabs__nav-wrap.is-top {
text-align: center;
}
/deep/ .dialog-dfl .el-tabs__nav-scroll {
display: inline-block;
}
/deep/ .dialog-dfl .el-tabs__item.is-top.is-active {
color: #f03e40ff;
margin-bottom: 7px;
}
/deep/ .dialog-dfl .el-tabs__item.is-top {
font-size: 18px;
height: 21px;
line-height: 21px;
color: #1c1f21;
}
/deep/ .dialog-dfl .el-slider__button {
position: relative !important;
width: 18px !important;
height: 65px !important;
border: 2px solid #fe3355;
border-radius: 4px;
transform: translateY(15%);
}
/deep/ .dialog-dfl .el-tabs__nav-wrap::after {
background: transparent;
}
/deep/ .dialog-dfl .el-slider__button::after {
position: absolute;
left: 5px;
top: 50%;
transform: translateY(-50%);
content: '';
background-color: #ebebeb;
border-radius: 1.5px;
height: 34px;
width: 3px;
}
/deep/ .dialog-dfl .el-slider__button::before {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
content: '';
background-color: #ebebeb;
border-radius: 1.5px;
height: 34px;
width: 3px;
}
/deep/ .dialog-dfl .el-slider__runway {
background-color: transparent !important;
height: 66px !important;
// top: -21px;
margin: 0px;
width: 99%;
}
/deep/ .el-slider__bar {
height: 66px !important;
// transform: translateY(-48%);
background-color: transparent;
}
/deep/ .dialog-dfl .el-slider {
height: 100% !important;
}
/deep/ .dialog-dfl .el-loading-spinner .path {
stroke: #fe3355 !important;
}
/deep/ .dialog-dfl .el-loading-spinner .el-loading-text {
color: #c1c1c1 !important;
margin-top: 10px;
}
/deep/ .el-slider__button {
height: 74px;
}
</style>
将视频的地址获取后端需要的id,模拟文件上传动作
import axios from 'axios'
import ak from '@/utils/common.js'
export default {
data() {
return {}
},
methods: {
async getossid(urlData, fileName = new Date().getTime() + 'cover.jpg') {
//获取提图片的ossid
let arr = urlData.split(',')
let mime = arr[0].match(/:(.*?);/)[1]
let bytes = atob(arr[1]) // 解码base64
let n = bytes.length
let ia = new Uint8Array(n)
while (n--) {
ia[n] = bytes.charCodeAt(n)
}
let file = new File([ia], fileName, { type: mime })
const url = process.env.BASE_API + '/ykb-oss/oss/uploadFdfs'
let formData = new FormData()
formData.append('files', new File([ia], fileName, { type: mime }))
formData.append('param', JSON.stringify({ appid: 'oss7wqc3k', appsecret: 'ok601gkg', project: 'lms' }))
const fileInfo = await axios({
method: 'post',
url,
// url: '/api/ykb-oss/oss/mergeChunkFile',
headers: {
Authorization: `Bearer ${this.$store.getters.token}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: formData
})
// this.firstImage = fileInfo.data.data[0].id
return fileInfo.data.data[0].id
// return new File([ia], fileName, { type: mime })
},
getImageBase64(blob) {
//blob转换为base64
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(blob)
reader.onload = () => {
const base64 = reader.result
resolve(base64)
}
reader.onerror = error => reject(error)
})
},
// 根据ossid获取文件实际的路径
async getUrlByOssid(ossId) {
const res = await axios({
method: 'post',
headers: {
Authorization: 'Bearer ' + localStorage.getItem('token')
},
url: process.env.BASE_API + '/ykb-oss/oss/getStoragePath',
data: { ossId: ossId, type: 'S00000002-200040' }
})
if (res.data.code !== 8200) {
if (res.data.message) {
ak.error(res.data.message)
return Promise.reject(res.data.message)
} else {
ak.error('通讯错误,请联系管理员')
return Promise.reject('通讯错误,请联系管理员')
}
} else {
return res.data.data
}
}
}
}
运用组件
<videoCover ref="videoCover" :file="videoForm.file" :is-show="videoForm.comIsShow" @closeDialog="close" @confirmImg="confirmImg"></videoCover>
<script>
import videoCover from './captur.vue'
//打开弹窗
openmodal() {
this.videoForm.comIsShow = true
if (Object.keys(this.videoForm.file).length != 0) {
this.$refs.videoCover.changeFile(this.videoForm.file, this.videoForm.duration, this.videoForm.videoUrl)
}
},
</script>