需求背景
由于项目的保密问题,文章不能写的太详细,本文只写核心实现逻辑,文中的demo也已经做脱敏处理。
最近遇到这么一个需求,需要纯前端实现将图片导出成视频。
大致功能如下图:
- 将网页的dom转成图片,导出成视频
- 前端上传图片,导出成视频
导出的视频需要能够正常播放
实现方案
要实现图片导出成视频,我们可以借助ffmpeg或者js的原生属性 MediaRecorder
ffmpeg
使用 ffmpeg
来生成视频灵活、高效。目前有一个开源项目 ffmpeg.wasm
,它将 ffmpeg
移植到了 WebAssembly,可以在浏览器中使用。但这个库对电脑性能有一定要求,也是导出视频一种比较好的方案。
MediaRecorder
使用js原生属性将图片导出成视频大致流程如下:
- 图像生成:使用
html2canvas
将DOM元素渲染为图像流或者借助input属性将上传的图片转换为图像流。 - 视频录制:使用
MediaRecorder
捕获canvas
的媒体流,并逐帧绘制图像。 - 视频输出:录制停止后,将录制的数据保存为
Blob
对象并保存为视频文件,同时在页面上播放视频。
本文将详细介绍如何使用浏览器原生属性生成视频。
代码实现
使用云编译器进行调试
以下demo,大家可以直接在豆包云vscode编译器中调试,云编译器免配置任何环境,可以直接使用。
必要依赖安装
npm i file-saver
npm i html2canvas
这两个包分别提供了在浏览器端实现文件保存、网页截图的功能。
基础框架搭建
我们主要演示如何将dom渲染成图片,导出视频
import React, {
useRef } from 'react';
import html2canvas from 'html2canvas';
import {
saveAs } from 'file-saver';
const App = () => {
const data = [
{
user: 'User1', text: 'This is a text message 1' },
{
user: 'User2', text: 'This is a text message 2' },
{
user: 'User3', text: 'This is a text message 3' },
];
const containerRef = useRef(null);
const exportVideo = async () => {
};
return (
<div>
<button onClick={
exportVideo}>导出视频</button>
<div ref={
containerRef}>
{
data.map((item, index) => (
<div key={
index} style={
{
border: '1px solid black', margin: '10px', padding: '10px' }}>
<strong>{
item.user}</strong>: {
item.text}
</div>
))}
</div>
</div>
);
};
export default App;
获取canvas媒体流
const exportVideo = async () => {
const frames = [];
// 生成每个DOM元素的图像
// 遍历containerRef中的所有子元素
const elements = containerRef.current.children;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
// 使用html2canvas将DOM元素渲染为canvas图像
const canvas = await html2canvas(element);
// 将生成的canvas图像添加到frames数组中
frames.push(canvas);
}
// 创建隐藏的canvas元素
// 这个canvas将用于绘制每一帧的图像并录制视频
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
const ctx = canvas.getContext('2d');
// 设置MediaRecorder来捕获canvas流
const stream = canvas.captureStream();
// ...
};
上述代码中,我们使用html2canvas将dom转成了流文件,借助canvas生成了媒体流,用于后续处理。如果是上传文件,需要如下处理
import React, {
useRef } from 'react';
const App = () => {
const canvasRef = useRef(null);
// 处理文件上传
const handleFileUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
// 文件读取完成后的回调
reader.onload = (e) => {
const img = new Image();
img.src = e.target.result;
// 图像加载完成后的回调
img.onload = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
// 将图像绘制到canvas上
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
};
};
// 读取文件为DataURL
reader.readAsDataURL(file);
}
};
return (
<div>
<input type="file" accept="image/*" onChange={
handleFileUpload} />
<canvas ref={
canvasRef}></canvas>
</div>
);
};
export default App;
FileReader
可以将上传的图片文件读取为数据URL,然后将其加载到 Image
对象中,并绘制到 canvas
上。
将媒体流导出为视频
const exportVideo = async () => {
// ....
// 设置MediaRecorder来捕获canvas流
// captureStream()方法将canvas的绘制内容捕获为媒体流
const stream = canvas.captureStream();
// 创建MediaRecorder实例,用于录制视频
// 设置视频的MIME类型为video/webm
const recorder = new MediaRecorder(stream, {
mimeType: "video/webm" });
const chunks = [];
// 当有数据可用时,将数据块添加到chunks数组中
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
// 当录制停止时,将所有数据块合并为一个Blob对象,并保存为视频文件
recorder.onstop = () => {
const blob = new Blob(chunks, {
type: "video/webm" });
// 使用file-saver库将Blob保存为文件
saveAs(blob, "video.webm");
};
// 开始录制
recorder.start();
// 遍历frames数组,将每一帧绘制到canvas上
for (const frame of frames) {
// 将当前帧绘制到canvas上
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
// 等待1秒,以便在视频中每帧显示1秒
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// 停止录制
recorder.stop();
};
增加视频预览功能
预览功能就是将生成好的流,生成一个src,使用video标签播放即可
import React, {
useRef } from "react";
import html2canvas from "html2canvas";
import {
saveAs } from "file-saver";
const App = () => {
// ...
const containerRef = useRef(null);
const videoRef = useRef(null);
const exportVideo = async () => {
// ...
recorder.onstop = () => {
const blob = new Blob(chunks, {
type: "video/webm" });
saveAs(blob, "video.webm");
// 视频播放核心代码
const url = URL.createObjectURL(blob);
videoRef.current.src = url;
videoRef.current.play();
};
// ...
};
return (
<div>
<button onClick={
exportVideo}>Export Video</button>
<div ref={
containerRef}>
{
data.map((item, index) => (
<div
key={
index}
style={
{
border: "1px solid black",
margin: "10px",
padding: "10px",
}}
>
<strong>{
item.user}</strong>: {
item.text}
</div>
))}
</div>
<video ref={
videoRef} width="640" height="480" controls></video>
</div>
);
};
export default App;
完整代码
import React, {
useRef } from "react";
import html2canvas from "html2canvas";
import {
saveAs } from "file-saver";
const App = () => {
const data = [
{
user: "User1", text: "This is a text message 1" },
{
user: "User2", text: "This is a text message 2" },
{
user: "User3", text: "This is a text message 3" },
];
const containerRef = useRef(null);
const videoRef = useRef(null);
const exportVideo = async () => {
const frames = [];
// 生成每个DOM元素的图像
// 遍历containerRef中的所有子元素
const elements = containerRef.current.children;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
// 使用html2canvas将DOM元素渲染为canvas图像
const canvas = await html2canvas(element);
// 将生成的canvas图像添加到frames数组中
frames.push(canvas);
}
// 创建隐藏的canvas元素
// 这个canvas将用于绘制每一帧的图像并录制视频
const canvas = document.createElement("canvas");
canvas.width = 640;
canvas.height = 480;
const ctx = canvas.getContext("2d");
// 设置MediaRecorder来捕获canvas流
// captureStream()方法将canvas的绘制内容捕获为媒体流
const stream = canvas.captureStream();
// 创建MediaRecorder实例,用于录制视频
// 设置视频的MIME类型为video/webm
const recorder = new MediaRecorder(stream, {
mimeType: "video/webm" });
const chunks = [];
// 当有数据可用时,将数据块添加到chunks数组中
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
// 当录制停止时,将所有数据块合并为一个Blob对象,并保存为视频文件
recorder.onstop = () => {
const blob = new Blob(chunks, {
type: "video/webm" });
// 使用file-saver库将Blob保存为文件
saveAs(blob, "video.webm");
// 创建视频的URL并在页面上播放视频
const url = URL.createObjectURL(blob);
videoRef.current.src = url;
videoRef.current.play();
};
// 开始录制
recorder.start();
// 遍历frames数组,将每一帧绘制到canvas上
for (const frame of frames) {
// 将当前帧绘制到canvas上
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
// 等待1秒,以便在视频中每帧显示1秒
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// 停止录制
recorder.stop();
};
return (
<div>
<button onClick={
exportVideo}>Export Video</button>
<div ref={
containerRef}>
{
data.map((item, index) => (
<div
key={
index}
style={
{
border: "1px solid black",
margin: "10px",
padding: "10px",
}}
>
<strong>{
item.user}</strong>: {
item.text}
</div>
))}
</div>
<video ref={
videoRef} width="640" height="480" controls></video>
</div>
);
};
export default App;
上述代码大家可以直接在豆包云编译器中运行查看效果:
总结
本文介绍了如何在前端将图片或网页DOM元素导出为视频。通过使用 html2canvas
将DOM元素渲染为图像,再借助 MediaRecorder
捕获 canvas
的媒体流,逐帧绘制图像并保存为视频文件。此方法无需依赖后端服务,实现了纯前端的图片导出视频功能。
文章还示范了如何处理上传的图片,并将其转化为视频。
关注我!前端学习好活泼!