Bootstrap
博客内容搜索
Kaysama's Blog

想必大家应该都见过类似这种字幕往下边叠的视频截图

共产党宣言

一般的做法都是自己把视频某几帧截图,然后用作图工具裁剪/覆盖之类的方式来制作出来。但是作为一个程序员,当然要思考一下效率更高的方式啊,于是就有了这篇博客。

功能目标:

直接本地选择视频,自定义字幕截取位置,自动添加字幕到第一帧的下方。

最终的页面布局如下:

技术要点:

1、通过 bolbURL 播放本地视频;

2、通过 **createImageBitmap **截取视频某部分

实现:

为了dom操作的方便,我这里就直接用vue来做了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// 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++
})
}
},
}
})

完整代码戳这里

在线演示1在线演示2