Node + FFmpeg 實現Canvas動畫導出視頻

導言

Canvas爲前端提供了動畫展現的平臺,隨着如今視頻娛樂的流行,你是否想過把Canvas動畫導出視頻?目前純前端的視頻編碼轉換(例如WebM Encoder Whammy)還存在許多限制,較爲成熟的方案是將每幀圖片傳給後端實現,由後端調用FFmpeg進行視頻轉碼。總體流程並不複雜,這篇文章將帶你們實現這個過程。前端

總體方案

  • 由前端記錄Canvas動畫的每幀圖像,以base64字符串形式傳給後端node

  • 利用node fluent-ffmpeg模塊,調用FFmpeg將圖片合併成視頻,並將視頻存儲在server端,並返回相應下載urlgit

  • 前端經過請求獲得視頻文件github

前端部分

每幀圖片生成

圖片生成能夠經過canvas原生接口toDataURL實現,最終返回base64形式的圖像數據。ajax

generatePng () {
  ...
  var imgData = canvas.toDataURL("image/png");
  return imgData;
}

動畫錄製與圖片流傳輸

動畫的記錄與傳送是個異步過程,這裏返回一個Promise,等待後端處理完畢,收到迴應後,即完成此異步過程。npm

如下代碼將canvas每幀動畫信息存入一個圖片數組imgs中,將數組轉成字符串的形式傳給後端。注意這裏contentType設置爲「text/plain」。canvas

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

相關文章
相關標籤/搜索