Canvas为前端提供了动画展示的平台,随着现在视频娱乐的流行,你是否想过把Canvas动画导出视频?目前纯前端的视频编码转换(例如WebM Encoder Whammy)还存在许多限制,较为成熟的方案是将每帧图片传给后端实现,由后端调用FFmpeg进行视频转码。整体流程并不复杂,这篇文章将带大家实现这个过程。
由前端记录Canvas动画的每帧图像,以base64字符串形式传给后端
利用node fluent-ffmpeg模块,调用FFmpeg将图片合并成视频,并将视频存储在server端,并返回相应下载url
前端通过请求得到视频文件
每帧图片生成
图片生成可以通过canvas原生接口toDataURL实现,最终返回base64形式的图像数据。
generatePng () {
var imgData = canvas.toDataURL("image/png");
return imgData;
动画录制与图片流传输
动画的记录与传送是个异步过程,这里返回一个Promise,等待后端处理完毕,收到回应后,即完成此异步过程。
以下代码将canvas每帧动画信息存入一个图片数组imgs中,将数组转成字符串的形式传给后端。注意这里contentType设置为“text/plain”。
generateVideo () {
var that = this;
return new Promise (
function (resolve, reject) {
var imgs = [];
window.requestAnimationFrame(that.recordTick.bind(that, imgs, resolve, reject));
recordTick (imgs, resolve, reject) {
...//每帧动画的记录信息,如时间戳等
if (...) {//动画终止条件
this.stopPlay();
imgs.push(this.generatePng());
$.ajax({
url: "/video/record",
data: imgs.join(" "),
method: "POST",
contentType: "text/plain",
success: function (data, textStatus, jqXHR) {
resolve(data);
error: function (jqXHR, textStatus, errorThrown) {
reject(errorThrown);
} else {
...//每帧动画展示的代码
imgs.push(this.generatePng());
window.requestAnimationFrame(this.recordTick.bind(this, imgs, resolve, reject));
上一节代码中,动画停止时,会通过post请求给后端传送所有图片数据,后端处理完毕后,返回数据中包含一个url,此url即为视频文件的下载地址。
为了支持浏览器端用户点击下载,我们需要用到a标签的download属性,此属性可以支持点击a标签后下载指定文件。
editor.generateVideo().then(function (data) {
videoRecordingModal.setDownloadLink(data.url, data.filename);
videoRecordingModal.changeStatus("recorded");
setDownloadLink: function (url, filename) {
this.config.$dom.find(".video-download").attr("href", url);
this.config.$dom.find(".video-download").attr("download", filename);
图片序列生成
接收到前端传送的图片数据后,我们首先需要将图片解析、存储在服务器中,我们建立以当前时间戳命名的文件夹,将图片序列以一定格式存储于其中。由于每张图片写入都是异步过程,为确保所有图片都已处理完毕后,才执行视频转码过程,我们需要用到Promise.all。
Promise.all(imgs.map(function (value, index) {
var img = decodeBase64Image(value)
var data = img.data
var type = img.type
return new Promise(function (resolve, reject) {
fs.writeFile(path.resolve(__dirname, (folder + "/img" + index + "." + type)), data, "base64", function(err) {
if (err) {
reject(err)
} else {
resolve()
})).then(function () {
…//视频转码
其中decodeBase64Image函数参考这里。
视频生成利用FFmpeg转码工具。
首先确保server端安装了FFmpeg
brew install ffmpeg
在项目中安装fluent-ffmpeg,这是node调用ffmpeg的接口模块
npm install fluent-ffmpeg --save
结合上一节图片序列存储的代码,整个接口代码如下:
app.post("/video/record", function(req, res) {
var imgs = req.text.split(" ")
var timeStamp = Date.now()
var folder = "images/" + timeStamp
if (!fs.existsSync(resolve(folder))){
fs.mkdirSync(resolve(folder));
Promise.all(imgs.map(function (value, index) {
var img = decodeBase64Image(value)
var data = img.data
var type = img.type
return new Promise(function (resolve, reject) {
fs.writeFile(path.resolve(__dirname, (folder + "/img" + index + "." + type)), data, "base64", function(err) {
if (err) {
reject(err)
} else {
resolve()
})).then(function () {
var proc = new ffmpeg({ source: resolve(folder + "/img%d.png"), nolog: true })
.withFps(25)
.on("end", function() {
res.status(200)
res.send({
url: "/video/mpeg/" + timeStamp,
filename: "jianshi" + timeStamp + ".mpeg"
.on("error", function(err) {
console.log("ERR: " + err.message)
.saveToFile(resolve("video/jianshi" + timeStamp + ".mpeg"))
最终将视频文件传输给前端的接口代码如下:
app.get("/video/mpeg/:timeStamp", function(req, res) {
res.contentType("mpeg");
var rstream = fs.createReadStream(resolve("video/jianshi" + req.params.timeStamp + ".mpeg"));
rstream.pipe(res, {end: true});
注:此功能是个人项目”简诗”的一部分,完整代码可以查看https://github.com/moyuer1992...
阅读 2803·2021-11-25 09:43
阅读 2854·2021-11-23 09:51
阅读 3341·2019-08-30 13:08
阅读 1306·2019-08-29 12:48
阅读 3402·2019-08-29 12:26
阅读 216·2019-08-28 18:16
阅读 2353·2019-08-26 13:45
阅读 2226·2019-08-26 12:15