资讯专栏INFORMATION COLUMN

Node + FFmpeg 实现Canvas动画导出视频

weij / 3070人阅读

摘要:动画录制与图片流传输动画的记录与传送是个异步过程,这里返回一个,等待后端处理完毕,收到回应后,即完成此异步过程。

导言

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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82328.html

相关文章

  • 使用 canvas 实现精灵动画

    摘要:文章首发于个人博客在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。 文章首发于个人博客:http://heavenru.com 在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。在这篇文章中,主要是介绍两个命令行工具来实现将一个短视频...

    岳光 评论0 收藏0
  • 使用 canvas 实现精灵动画

    摘要:文章首发于个人博客在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。 文章首发于个人博客:http://heavenru.com 在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。在这篇文章中,主要是介绍两个命令行工具来实现将一个短视频...

    lastSeries 评论0 收藏0
  • 使用 canvas 实现精灵动画

    摘要:文章首发于个人博客在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。 文章首发于个人博客:http://heavenru.com 在最近项目中需要实现一个精灵动画,素材方只提供了一个短视频素材,所以在实现精灵动画之前先介绍两个工具来帮助我们更好的实现需求。在这篇文章中,主要是介绍两个命令行工具来实现将一个短视频...

    call_me_R 评论0 收藏0
  • 庆祝新年?画一颗圣诞树?还是...

    摘要:关于节日圣诞节,元旦,看大家情侣在朋友圈里发各种庆祝的或者祝福的话语,甚是感动,然后悄悄拉黑了。预览效果本地下打开很卡,火狐正常圣诞树早先的时候是圣诞节的时候,看到各种用字符组成圣诞树的形式,于是自己就去试了下,还是比较简单的。 关于节日 圣诞节,元旦,看大家(情侣)在朋友圈里发各种庆祝的或者祝福的话语,甚是感动,然后悄悄拉黑了。作为单身狗,我们也有自己庆祝节日的方式,今天我们就来实现...

    cloud 评论0 收藏0

发表评论

0条评论

weij

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<