Node + FFmpeg 完成Canvas动画导出视频

导言

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});
})

结果预览

《Node + FFmpeg 完成Canvas动画导出视频》

注:此功用是个人项目”简诗”的一部份,完全代码能够检察https://github.com/moyuer1992…

    原文作者:摸鱼儿
    原文地址: https://segmentfault.com/a/1190000008935586
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞