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

开发 前端
Canvas为前端提供了动画展示的平台,随着现在视频娱乐的流行,你是否想过把Canvas动画导出视频?目前纯前端的视频编码转换(例如WebM Encoder Whammy)还存在许多限制,较为成熟的方案是将每帧图片传给后端实现,由后端调用FFmpeg进行视频转码。整体流程并不复杂,这篇文章将带大家实现这个过程。

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

导言

Canvas为前端提供了动画展示的平台,随着现在视频娱乐的流行,你是否想过把Canvas动画导出视频?目前纯前端的视频编码转换(例如WebM Encoder Whammy)还存在许多限制,较为成熟的方案是将每帧图片传给后端实现,由后端调用FFmpeg进行视频转码。整体流程并不复杂,这篇文章将带大家实现这个过程。

整体方案

  • 由前端记录Canvas动画的每帧图像,以base64字符串形式传给后端
  • 利用node fluent-ffmpeg模块,调用FFmpeg将图片合并成视频,并将视频存储在server端,并返回相应下载url
  • 前端通过请求得到视频文件

前端部分

每帧图片生成

图片生成可以通过canvas原生接口toDataURL实现,最终返回base64形式的图像数据。 

  1. generatePng () { 
  2.   ... 
  3.   var imgData = canvas.toDataURL("image/png"); 
  4.   return imgData; 
  5.  

动画录制与图片流传输

动画的记录与传送是个异步过程,这里返回一个Promise,等待后端处理完毕,收到回应后,即完成此异步过程。

以下代码将canvas每帧动画信息存入一个图片数组imgs中,将数组转成字符串的形式传给后端。注意这里contentType设置为“text/plain”。

  1. generateVideo () { 
  2.   var that = this; 
  3.   return new Promise ( 
  4.     function (resolve, reject) { 
  5.       var imgs = []; 
  6.       ... 
  7.       window.requestAnimationFrame(that.recordTick.bind(that, imgs, resolve, reject)); 
  8.     } 
  9.   ) 
  10.   
  1. recordTick (imgs, resolve, reject) { 
  2.   ...//每帧动画的记录信息,如时间戳等 
  3.  
  4.   if (...) {//动画终止条件 
  5.     this.stopPlay(); 
  6.     imgs.push(this.generatePng()); 
  7.     $.ajax({ 
  8.       url: '/video/record'
  9.       data: imgs.join(' '), 
  10.       method: 'POST'
  11.       contentType: 'text/plain'
  12.       success: function (data, textStatus, jqXHR) { 
  13.         resolve(data); 
  14.       }, 
  15.       error: function (jqXHR, textStatus, errorThrown) { 
  16.         reject(errorThrown); 
  17.       } 
  18.     }); 
  19.   } else { 
  20.     ...//每帧动画展示的代码 
  21.  
  22.     imgs.push(this.generatePng()); 
  23.     window.requestAnimationFrame(this.recordTick.bind(this, imgs, resolve, reject)); 
  24.   } 
  25.  

视频下载

上一节代码中,动画停止时,会通过post请求给后端传送所有图片数据,后端处理完毕后,返回数据中包含一个url,此url即为视频文件的下载地址。

为了支持浏览器端用户点击下载,我们需要用到a标签的download属性,此属性可以支持点击a标签后下载指定文件。

  1. editor.generateVideo().then(function (data) { 
  2.   videoRecordingModal.setDownloadLink(data.url, data.filename); 
  3.   videoRecordingModal.changeStatus('recorded'); 
  4. });   
  1. setDownloadLink: function (url, filename) { 
  2.   this.config.$dom.find('.video-download').attr('href', url); 
  3.   this.config.$dom.find('.video-download').attr('download', filename); 
  4.  

后端部分

图片序列生成

接收到前端传送的图片数据后,我们首先需要将图片解析、存储在服务器中,我们建立以当前时间戳命名的文件夹,将图片序列以一定格式存储于其中。由于每张图片写入都是异步过程,为确保所有图片都已处理完毕后,才执行视频转码过程,我们需要用到Promise.all。

  1. Promise.all(imgs.map(function (value, index) { 
  2.   var img = decodeBase64Image(value) 
  3.   var data = img.data 
  4.   var type = img.type 
  5.   return new Promise(function (resolve, reject) { 
  6.     fs.writeFile(path.resolve(__dirname, (folder + '/img' + index + '.' + type)), data, 'base64'function(err) { 
  7.       if (err) { 
  8.         reject(err) 
  9.       } else { 
  10.         resolve() 
  11.       } 
  12.     }) 
  13.   }) 
  14. })).then(function () { 
  15.   …//视频转码 
  16. })  

其中decodeBase64Image函数参考这里

视频生成

视频生成利用FFmpeg转码工具。首先确保server端安装了FFmpeg

  1. brew install ffmpeg 

在项目中安装fluent-ffmpeg,这是node调用ffmpeg的接口模块

  1. npm install fluent-ffmpeg --save 

结合上一节图片序列存储的代码,整个接口代码如下:

  1. app.post('/video/record'function(req, res) { 
  2.   var imgs = req.text.split(' '
  3.   var timeStamp = Date.now() 
  4.   var folder = 'images/' + timeStamp 
  5.   if (!fs.existsSync(resolve(folder))){ 
  6.     fs.mkdirSync(resolve(folder)); 
  7.   } 
  8.  
  9.   Promise.all(imgs.map(function (value, index) { 
  10.     var img = decodeBase64Image(value) 
  11.     var data = img.data 
  12.     var type = img.type 
  13.     return new Promise(function (resolve, reject) { 
  14.       fs.writeFile(path.resolve(__dirname, (folder + '/img' + index + '.' + type)), data, 'base64'function(err) { 
  15.         if (err) { 
  16.           reject(err) 
  17.         } else { 
  18.           resolve() 
  19.         } 
  20.       }) 
  21.     }) 
  22.   })).then(function () { 
  23.     var proc = new ffmpeg({ source: resolve(folder + '/img%d.png'), nolog: true }) 
  24.       .withFps(25) 
  25.       .on('end'function() { 
  26.         res.status(200) 
  27.         res.send({ 
  28.           url: '/video/mpeg/' + timeStamp
  29.           filename: 'jianshi' + timeStamp + '.mpeg' 
  30.         }) 
  31.       }) 
  32.       .on('error'function(err) { 
  33.         console.log('ERR: ' + err.message) 
  34.       }) 
  35.       .saveToFile(resolve('video/jianshi' + timeStamp + '.mpeg')) 
  36.   }) 
  37. })  

视频下载

最终将视频文件传输给前端的接口代码如下:

  1. app.get('/video/mpeg/:timeStamp'function(req, res) { 
  2.   res.contentType('mpeg'); 
  3.   var rstream = fs.createReadStream(resolve('video/jianshi' + req.params.timeStamp + '.mpeg')); 
  4.   rstream.pipe(res, {endtrue}); 
  5. })  

效果预览 

  

注:此功能是个人项目”简诗”的一部分,完整代码可以查看https://github.com/moyuer1992...

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2022-03-09 09:00:41

SwiftUI视图生成器Swift

2012-07-13 13:52:54

Canvas

2022-08-29 10:39:32

FFmpeg多媒体框架开源

2022-08-11 09:30:52

transitionCSS

2021-12-02 15:14:02

ffmpeg视频Python

2009-12-29 10:06:09

WPF Canvas

2022-06-29 14:06:54

canvas鸿蒙

2024-02-27 13:03:38

前端视频合成FFmpeg

2012-07-11 15:54:59

canvas

2012-07-13 13:41:35

Canvas

2015-10-08 08:48:44

HTML5canvas动画

2016-01-20 10:11:56

华丽CanvasHTML5

2022-11-28 17:22:02

CanvasCSS

2021-02-21 07:49:40

Web动画SVG线条动画

2016-10-20 15:54:12

CanvasHtml5Javascript

2022-07-05 16:21:44

绘制表盘鸿蒙

2011-07-08 10:15:15

IPhone 动画

2022-03-10 11:04:04

Vue3Canvas前端

2021-12-31 19:36:27

js控制台动画

2012-11-30 10:42:00

IBMdW
点赞
收藏

51CTO技术栈公众号