Skip to content

Commit 8b13e5b

Browse files
committed
add video-frame2
1 parent 506cc12 commit 8b13e5b

4 files changed

Lines changed: 217 additions & 4 deletions

File tree

.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default defineConfig({
5656
base: '/blog/front/demo/',
5757
items: [
5858
{ text: '视频帧渲染', link: 'video-editor/video-frame/' },
59+
{ text: '视频帧渲染2', link: 'video-editor/video-frame2/' },
5960
{ text: '音视频波形图渲染', link: 'video-editor/waveform/' },
6061
],
6162
},
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<div class="container">
2+
<div>
3+
<h3>
4+
先选择一个视频
5+
<input type="file" accept="video/*" id="file">
6+
</h3>
7+
</div>
8+
9+
<h3>渲染全部视频帧</h3>
10+
<div id="frame-container">
11+
<div class="play-line"></div>
12+
</div>
13+
</div>
14+
15+
<script>
16+
const onFileChange = (callback) => {
17+
if (onFileChange._callbacks) onFileChange._callbacks.push(callback);
18+
else (onFileChange._callbacks = [callback]);
19+
};
20+
shadowDocument.querySelector('#file').addEventListener('change', (event) => {
21+
const url = URL.createObjectURL(event.target.files[0]);
22+
onFileChange._callbacks?.forEach((callback) => callback(url));
23+
});
24+
25+
class VideoFrame {
26+
video = document.createElement('video');
27+
canvas = document.createElement('canvas');
28+
status = 'init';
29+
frameList = [];
30+
31+
// 渲染视频全部帧容器
32+
container = null;
33+
// 每秒像素
34+
pxPerSecond = 0;
35+
36+
constructor ({
37+
container,
38+
width,
39+
height,
40+
pxPerSecond,
41+
url,
42+
}) {
43+
this.container = container;
44+
const { width: containerWidth, height: containerHeight } = container.getBoundingClientRect();
45+
this.canvas.width = width ?? containerWidth;
46+
this.canvas.height = height ?? containerHeight;
47+
pxPerSecond && (this.pxPerSecond = pxPerSecond);
48+
49+
container.appendChild(this.canvas);
50+
51+
this.onLoadedmetadata = this.onLoadedmetadata.bind(this);
52+
this.video.addEventListener('loadedmetadata', this.onLoadedmetadata);
53+
url && this.load(url);
54+
}
55+
56+
async onLoadedmetadata () {
57+
this.status = 'loaded';
58+
this.pxPerSecond && (this.canvas.width = this.pxPerSecond * this.video.duration);
59+
await this.getFrameData();
60+
this.drawFrame();
61+
}
62+
63+
async load ({ url, width, height, pxPerSecond }) {
64+
this.status = 'loadeding';
65+
this.video.src = url;
66+
this.video.load();
67+
68+
if (width) {
69+
this.canvas.width = width;
70+
}
71+
if (height) {
72+
this.canvas.height = height;
73+
}
74+
if (pxPerSecond) {
75+
this.pxPerSecond = pxPerSecond;
76+
}
77+
78+
await new Promise((resolve, reject) => {
79+
const _loadedmetadata = () => {
80+
this.video.removeEventListener('loadedmetadata', _loadedmetadata);
81+
resolve(true);
82+
};
83+
const _error = () => {
84+
this.video.removeEventListener('error', _error);
85+
reject(false);
86+
};
87+
this.video.addEventListener('loadedmetadata', _loadedmetadata);
88+
this.video.addEventListener('error', _error);
89+
});
90+
}
91+
92+
async render ({
93+
width = this.canvas.width,
94+
height = this.canvas.height,
95+
pxPerSecond = this.pxPerSecond,
96+
...props
97+
} = {}) {
98+
if (pxPerSecond) {
99+
width = pxPerSecond * this.video.duration;
100+
}
101+
if (props.canvas) {
102+
props.canvas.width = width;
103+
props.canvas.height = height;
104+
} else {
105+
this.canvas.width = width;
106+
}
107+
await this.getFrameData(width);
108+
this.drawFrame();
109+
}
110+
111+
async getFrameData (width = this.canvas.width) {
112+
const videoWidth = this.video.videoWidth;
113+
const videoHeight = this.video.videoHeight;
114+
const duration = this.video.duration;
115+
const canvasWidth = width ?? this.canvas.width;
116+
const canvasHeight = this.canvas.height;
117+
118+
const frameHeight = canvasHeight;
119+
const frameWidth = videoWidth * frameHeight / videoHeight;
120+
121+
// 计算时间轴需要多少帧
122+
const frameCount = Math.ceil(canvasWidth / frameWidth);
123+
// 计算帧间隔
124+
const interval = duration / frameCount;
125+
this.frameList = [];
126+
127+
// 创建离屏canvas
128+
const offscreenCanvas = document.createElement('canvas');
129+
const offscreenCtx = offscreenCanvas.getContext('2d');
130+
offscreenCanvas.width = frameWidth;
131+
offscreenCanvas.height = frameHeight;
132+
133+
for (let i = 0; i < frameCount; i++) {
134+
try {
135+
// 跳转到指定时间点
136+
this.video.currentTime = i * interval;
137+
138+
// 等待视频帧可用
139+
await new Promise((resolve) => {
140+
const onSeeked = () => {
141+
this.video.removeEventListener('seeked', onSeeked);
142+
143+
offscreenCtx.drawImage(
144+
this.video,
145+
0,
146+
0,
147+
frameWidth,
148+
frameHeight,
149+
);
150+
this.frameList.push({
151+
width: frameWidth,
152+
height: frameHeight,
153+
frame: offscreenCtx.getImageData(0, 0, frameWidth, frameHeight),
154+
});
155+
156+
resolve(true);
157+
};
158+
159+
this.video.addEventListener('seeked', onSeeked);
160+
});
161+
} catch (error) {
162+
console.error('生成帧预览时出错:', error);
163+
}
164+
}
165+
this.status = 'frameRendered';
166+
}
167+
168+
drawFrame ({
169+
canvas = this.canvas,
170+
} = {}) {
171+
const ctx = canvas.getContext('2d');
172+
this.frameList.forEach(({ width, height, frame }, index) => {
173+
ctx.putImageData(frame, width * index, 0);
174+
});
175+
}
176+
}
177+
178+
// 因为是在shadow环境 所以这里使用了注入进来的 shadowDocument 去获取元素
179+
const frameContainer = shadowDocument.querySelector('#frame-container');
180+
181+
const videoFrame = new VideoFrame({
182+
container: frameContainer,
183+
});
184+
185+
onFileChange((url) => {
186+
videoFrame.load({ url });
187+
});
188+
</script>
189+
190+
<style>
191+
.container {
192+
padding: 20px;
193+
}
194+
#frame-container,
195+
#frame-container-full {
196+
position: relative;
197+
height: 80px;
198+
background-color: #999;
199+
}
200+
</style>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
aside: false
3+
---
4+
5+
# 视频帧绘制
6+
7+
:::codeview
8+
<<< ./code.html
9+
:::

blog/front/demo/video-editor/waveform/code.html

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,19 @@ <h3>渲染波形图</h3>
9393
url && this.load(url);
9494
}
9595

96-
async load ({ url, width, height }) {
96+
async load ({ url, width, height, unitWidth }) {
9797
if (width) {
9898
this.canvas.width = width;
9999
}
100100
if (height) {
101101
this.canvas.height = height;
102102
}
103+
if (unitWidth) {
104+
this.unitWidth = unitWidth;
105+
}
103106

104107
await this.getChannelData(url);
105-
this.renderWaveform();
108+
this.render();
106109
}
107110

108111
clear () {
@@ -125,7 +128,7 @@ <h3>渲染波形图</h3>
125128
return this.channelData;
126129
}
127130

128-
renderWaveform ({
131+
render ({
129132
width = this.canvas.width,
130133
height = this.canvas.height,
131134
unitWidth = this.unitWidth,
@@ -221,7 +224,7 @@ <h3>渲染波形图</h3>
221224
const config = {};
222225

223226
const render = () => {
224-
waveform.renderWaveform(config);
227+
waveform.render(config);
225228
shadowDocument.querySelector('#code-config').innerHTML = JSON.stringify(config, undefined, 2);
226229
}
227230
shadowDocument.querySelector('#config-form').addEventListener('change', ({ target }) => {

0 commit comments

Comments
 (0)