视频取帧,设置封面、vue2+ Element+ vue-cropper+vue-core-video-player
main。js
import VueCoreVideoPlayer from 'vue-core-video-player'
Vue.use(VueCoreVideoPlayer, {
lang: 'zh-CN'
})
界面应用组件
<template>
<div class="box">
<h1>视频帧提取封面</h1>
<video class="videos" controls :src="videoForm.videoUrl"></video>
<div class="btns">
<el-button size="large" class="button">
选择视频文件
<input id="video-file" type="file" accept="video/*" @change="fileChange" />
</el-button>
<el-button size="large" color="#fe3355" style="color: red; position: relative; z-index: 999" @click="shows">
提取封面
</el-button>
</div>
<videoCover ref="videoCover" :file="videoForm.file" :is-show="videoForm.comIsShow" @closeDialog="close" @confirmImg="confirmImg"></videoCover>
<div class="look_img">
<img :src="imgLookUrl" alt="" />
</div>
</div>
</template>
<script>
import videoCover from './video.vue'
export default {
components: {
videoCover
},
data() {
return {
videoForm: {
videoUrl: '',
file: {},
comIsShow: false
},
imgLookUrl: ''
}
},
created() {},
methods: {
fileChange(e) {
let videoFile = e.target.files[0]
if (videoFile) {
this.videoForm.videoUrl = URL.createObjectURL(videoFile)
this.videoForm.file = videoFile
this.$refs.videoCover.changeFile(videoFile)
console.log(videoFile, 'videoFilevideoFile', typeof videoFile)
}
},
// 打开组件
shows() {
this.videoForm.comIsShow = true
this.$refs.videoCover.changeFile(this.videoForm.file)
console.log(3333)
},
//关闭组件回调
close() {
this.videoForm.comIsShow = false
},
//确认封面回调 data返回值
confirmImg(data) {
this.imgLookUrl.value = data.url
console.log(data)
// console.log(blobUrl);
}
}
}
</script>
<style lang="scss" scoped>
.button {
position: relative;
}
.button input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.box {
h1 {
font-size: 40px;
text-align: center;
margin-bottom: 50px;
// color: #fe3355;
font-weight: normal;
margin-top: 60px;
}
.videos {
width: 200px;
height: 100px;
margin: 0 auto;
display: block;
margin-bottom: 30px;
}
.btns {
display: flex;
align-items: center;
justify-content: center;
margin-top: 50px;
}
}
.look_img {
display: flex;
align-items: center;
justify-content: center;
margin-top: 50px;
img {
width: auto;
height: 200px;
}
}
</style>
组件分装
<template>
<div>
<el-dialog :visible.sync="isShow" title="" :close-on-click-modal="false" width="1000px" class="dialog-dfl" :before-close="beforeClose">
<div>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="封面截取" name="one">
<div class="conts" style="height: 450px">
<div v-show="!loading">
<div class="look_img">
<img :src="imgForm.url" alt="" />
</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">
000898{
{ imgForm.videoTime }} {
{ sliderVal }}
<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="cropperForm.loadings" element-loading-background="#fff" element-loading-text="解析中" style="height: 450px">
<div v-if="!cropperForm.loadings" class="conts_right">
<div class="l">
<div
:style="{
backgroundColor: cropperForm.bgColor
}"
v-html="cropperForm.imgLookUrl"
></div>
<!-- {
{ cropperForm.imgLookUrl }} -->
</div>
<div class="r">
<vueCropper ref="cropperRef" :img="imgForm.urlTwo" :output-size="option.outputSize" :output-type="option.outputType" :info="option.info" :can-scale="option.canScale" :auto-crop="option.autoCrop" :auto-crop-width="option.autoCropWidth" :auto-crop-height="option.autoCropHeight" :fixed-box="option.fixedBox" :fixed="option.fixed" :fixed-number="option.fixedNumber" :can-move="option.canMove" :can-move-box="option.canMoveBox" :original="option.original" :center-box="option.centerBox" :info-true="option.infoTrue" :full="option.full" :enlarge="option.enlarge" :mode="option.mode" :fill-color="cropperForm.bgColor" @real-time="previewHandle"></vueCropper>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<span class="dialog-footer">
<el-button v-show="activeName == 'one'" color="#fe3355" @click="confirmCover">
确认封面
</el-button>
<div v-show="activeName == 'two'" class="bottom_btn">
<div class="l">
<el-button style="margin-right: 10px; position: relative">
<span>选择本地封面</span>
<input id="img-file" type="file" accept="image/*" @change="fileChange" />
</el-button>
<el-color-picker v-model="cropperForm.bgColor" :locale="'zhCn'" show-alpha />
</div>
<div class="r">
<el-button color="#fe3355" @click="confirmCoverTwo">
确认封面
</el-button>
</div>
</div>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import { VueCropper } from 'vue-cropper'
export default {
components: { VueCropper },
props: {
// 验证码类型: true:不校验手机号是否在库; false:校验手机号是否在库
isShow: {
type: Boolean,
default: () => true
}
//传过来的视频文件
// file: {
// type: Object,
// default: () => ({})
// }
},
data() {
return {
loading: false,
file: null,
activeName: 'one',
sliderVal: 0,
sliderVal2: 20,
cropperRef: {},
imgForm: {
url: '', //封面预览地址
urlTwo: '', //封面预览地址
blob: {}, //封面blob对象
img_list: [], //底部预览条图片数组
videoTime: 0, //视频时长
oldVideoFile: {} //旧的视频文件
},
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,
},
cropperForm: {
imgLookUrl: '', //裁剪实时预览
bgColor: '#fff', //裁剪图片底色
loadings: true
}
}
},
watch: {
file(newVal, oldVal) {
console.log(oldVal, 'file')
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)
}
},
isShow(newVal, oldVal) {
console.log(newVal, oldVal)
// 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: {
changeFile(newVal) {
console.log('=-=-=-', this.isShow, this.file, newVal)
this.file = newVal
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'
console.log('---changeFile', newVal)
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.blob = {}
this.imgForm.img_list = []
this.imgForm.videoTime = 0
this.imgForm.oldVideoFile = this.file
let reader = new FileReader()
console.log(this.file, '0-0-0-')
let that = this
//获取视频时长
reader.onload = function(e) {
let video = document.createElement('video')
// @ts-ignore
video.src = e.target.result
video.addEventListener('loadedmetadata', async function() {
// 这里先看900宽度能放几张图片
const img_src = await that.captureFrame(that.file, Math.floor(video.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 = 90 * aspectRatio
let count = Math.floor(960 / 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.imgForm.urlTwo = res.url
that.imgForm.blob = res.blob
}
that.imgForm.img_list.push(res.url)
})
that.$nextTick(() => {
setTimeout(() => {
that.loading = false
}, 2000)
})
}
})
}
// @ts-ignore
reader.readAsDataURL(this.file)
},
//滑块位置改变 更滑上方主封面图
async sliderChange(val) {
const res = await this.captureFrame(this.file, val)
console.log(val, res.url, 'sliderChange')
this.imgForm.url = res.url
this.imgForm.urlTwo = res.url
this.imgForm.blob = res.blob
},
// 格式化提示时间
formatTooltip(val) {
console.log(val)
var timeString = this.convertSeconds(val)
return timeString
},
async handleClick(tab, event) {
console.log(tab, event)
if (this.activeName.value == 'two') {
this.cropperForm.loadings = true
this.$nextTick(() => {
setTimeout(() => {
this.cropperForm.loadings = false
}, 500)
})
}
},
//关闭模态弹窗
beforeClose() {
this.$emit('closeDialog', false)
},
//确认封面选择封面
confirmCover() {
this.$emit('closeDialog', false)
this.$emit('confirmImg', {
url: this.imgForm.url,
blob: this.imgForm.blob
})
},
// 获取视频帧的封面
captureFrame(videoFile, time = 0) {
return new Promise(succeed => {
const video = document.createElement('video')
console.log('captureFrame', time)
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 = Math.floor(seconds / 3600)
var minutes = Math.floor((seconds % 3600) / 60)
// var remainingSeconds = Math.floor(seconds % 60) //秒
var millisecond = Math.floor((seconds % 60) * 10) //毫秒
var timeString = ''
console.log(millisecond)
if (hours > 0) {
timeString += hours + ':'
}
timeString += minutes + ':' + millisecond
return timeString
},
//裁剪功能实时事件
previewHandle(val) {
console.log(val)
this.cropperForm.imgLookUrl = val.html
},
//本地上传封面
fileChange(e) {
let imgFile = e.target.files[0]
if (imgFile) {
this.cropperForm.loadings = true
this.imgForm.urlTwo = URL.createObjectURL(imgFile)
this.$nextTick(() => {
setTimeout(() => {
this.cropperForm.loadings = false
}, 500)
})
}
},
//本地封面确定事件
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: 450px;
box-sizing: border-box;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: center;
.look_img {
display: flex;
justify-content: center;
margin-bottom: 45px;
img {
width: auto !important;
height: 256px !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: 88px !important;
}
}
}
}
.slider-dfl {
position: absolute;
left: 0;
top: 0;
width: 100%;
}
.conts_right {
height: 450px;
box-sizing: border-box;
padding: 0 20px;
padding-bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
.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;
}
}
.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;
}
/deep/ .dialog-dfl .el-dialog__header {
padding: 0 !important;
}
/deep/ .dialog-dfl .el-dialog__body {
padding: 0 !important;
}
/deep/ .dialog-dfl .el-dialog__headerbtn {
z-index: 3;
}
/deep/ .dialog-dfl .el-dialog__footer {
padding: 15px 24px !important;
border-top: 1px solid #e4e7ed !important;
}
/deep/ .dialog-dfl .el-tabs__item {
height: 60px !important;
font-size: 16px !important;
}
/deep/ .dialog-dfl .el-tabs__nav-wrap::after {
height: 1px !important;
}
/deep/ .dialog-dfl .el-tabs__item:hover {
color: #fe3355 !important;
}
/deep/ .dialog-dfl .el-tabs__nav-wrap {
padding-left: 20px !important;
}
/deep/ .dialog-dfl .el-tabs__active-bar {
background-color: #fe3355 !important;
height: 3px !important;
}
/deep/ .dialog-dfl .el-tabs__item.is-active {
color: #fe3355 !important;
}
/deep/ .dialog-dfl .el-slider__button {
position: relative !important;
width: 24px !important;
height: 94px !important;
border: 2px solid #fe3355;
border-radius: 4px;
transform: translateY(12%);
}
/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: 88px !important;
top: -31px;
}
/deep/ .el-slider__bar {
height: 88px !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>