想必大家应该都见过类似这种字幕往下边叠的视频截图
一般的做法都是自己把视频某几帧截图,然后用作图工具裁剪/覆盖之类的方式来制作出来。但是作为一个程序员,当然要思考一下效率更高的方式啊,于是就有了这篇博客。
功能目标:
直接本地选择视频,自定义字幕截取位置,自动添加字幕到第一帧的下方。
最终的页面布局如下:
技术要点:
1、通过 bolbURL 播放本地视频;
2、通过 createImageBitmap 截取视频某部分
实现:
为了dom操作的方便,我这里就直接用vue来做了。
// 16进制字符串转rgb
function hex2rgb (hexStr) {
const value = parseInt(hexStr.replace('#', ''), 16)
const r = value >> 16 // 获取17~24位
const g = value >> 8 & 0xff // 获取9~16位
const b = value & 0xff // 获取0~8位
return { r, g, b }
}
let $canvas
let $video
let ctx
let drawTimes = -1
let fullHeight = 0
const offscreenCvs = new OffscreenCanvas(0, 0)
const offscreenCtx = offscreenCvs.getContext('2d')
window.$vm = new Vue({
el: '#app',
data: function () {
return {
controls: true,
video: {
width: 200,
height: 200,
},
videoHeight: 0,
ccForceShow: true,
ccShow: false,
ccShowTimer: -1,
exportScale: 1,
cc: {
left: 0,
top: 0,
width: 0,
height: 0,
color: '#009688',
},
fileName: '选择文件'
}
},
computed: {
canvasStyle () {
return {
width: `${this.video.width * this.exportScale}px`,
height: 'auto',
}
},
videoStyle () {
return {
width: `${this.video.width * this.exportScale}px`,
height: `${this.video.height * this.exportScale}px`,
}
},
ccStyle () {
return {
left: `${this.cc.left * this.exportScale}px`,
top: `${this.cc.top * this.exportScale}px`,
width: `${this.cc.width * this.exportScale}px`,
height: `${this.cc.height * this.exportScale}px`,
backgroundColor: `${this.ccColor}`,
}
},
ccColor () {
const rgb = hex2rgb(this.cc.color)
return `rgba(${rgb.r},${rgb.g},${rgb.b},0.5)`
},
},
mounted () {
$video = this.$refs.video
$canvas = this.$refs.canvas
ctx = $canvas.getContext('2d')
},
methods: {
onFileChange (e) {
const $fileVideo = this.$refs.fileVideo
const file = $fileVideo.files[0]
this.fileName = file.name
$video.src = URL.createObjectURL(file)
},
onCcChange (e) {
this.ccShow = true
clearTimeout(this.ccShowTimer)
this.ccShowTimer = setTimeout(() => {
this.ccShow = false
}, 1000)
},
onVideoMetaLoad () {
const $video = this.$refs.video
const videoWidth = Math.floor($video.videoWidth)
const videoHeight = Math.floor($video.videoHeight)
this.video = {
width: videoWidth,
height: videoHeight,
}
const ccHeight = 50
const ccLeft = 0
const ccWidth = Math.floor(videoWidth)
const ccTop = videoHeight - ccHeight - 15
$canvas.width = videoWidth
$canvas.height = videoHeight
this.cc = {
width: ccWidth,
height: ccHeight,
left: ccLeft,
top: ccTop,
color: this.cc.color,
}
drawTimes = 0
fullHeight = videoHeight
this.exportScale = 1
},
drawVideo () {
const video = this.video
const cc = {
left: Math.floor(this.cc.left),
top: Math.floor(this.cc.top),
width: Math.floor(this.cc.width),
height: Math.floor(this.cc.height),
}
if (drawTimes === -1) {
return alert('请先导入视频')
}
if (drawTimes === 0) {
ctx.drawImage($video, 0, 0, video.width, video.height)
drawTimes++
}
else {
const newHeight = fullHeight + cc.height
offscreenCvs.width = video.width
offscreenCvs.height = fullHeight
offscreenCtx.drawImage($canvas, 0, 0, video.width, fullHeight)
$canvas.height = newHeight
ctx.drawImage(offscreenCvs, 0, 0, video.width, fullHeight)
createImageBitmap($video, cc.left, cc.top, cc.width, cc.height)
.then(res => {
ctx.drawImage(res, cc.left, fullHeight, cc.width, cc.height)
fullHeight = newHeight
drawTimes++
})
}
},
}
})