当前位置: 首页 > news >正文

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

效果预览

图片描述

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

相关文章:

  • 数据库架构设计思路
  • 前端学习 -- Css -- 文本标签
  • Android开发专业名词及工具概述
  • 斐波那契数列——摘自搜狗百科
  • linux磁盘管理命令
  • 数据挖掘之数据准备——丢失数据
  • 今天加入云溪社区啦
  • 框架中无效的列类型异常分析
  • 起床继续编程
  • Linux主流架构运维工作简单剖析
  • AndroidStudio打包apk,安装出现签名冲突--解决办法
  • 最大整数
  • mysql sum() 求和函数的用法
  • 新事物的代价 共享汽车所碰到的尴尬
  • Intellij IDEA 配置Subversion插件时效解决方法
  • 《网管员必读——网络组建》(第2版)电子课件下载
  • 【node学习】协程
  • ES6简单总结(搭配简单的讲解和小案例)
  • gcc介绍及安装
  • HashMap剖析之内部结构
  • js对象的深浅拷贝
  • 动态规划入门(以爬楼梯为例)
  • 聊聊spring cloud的LoadBalancerAutoConfiguration
  • 配置 PM2 实现代码自动发布
  • 七牛云 DV OV EV SSL 证书上线,限时折扣低至 6.75 折!
  • 微服务框架lagom
  • 原生js练习题---第五课
  • Salesforce和SAP Netweaver里数据库表的元数据设计
  • 积累各种好的链接
  • ​Java并发新构件之Exchanger
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (二)Pytorch快速搭建神经网络模型实现气温预测回归(代码+详细注解)
  • (附源码)spring boot儿童教育管理系统 毕业设计 281442
  • (深度全面解析)ChatGPT的重大更新给创业者带来了哪些红利机会
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • (转)socket Aio demo
  • (转)如何上传第三方jar包至Maven私服让maven项目可以使用第三方jar包
  • (最简单,详细,直接上手)uniapp/vue中英文多语言切换
  • ..thread“main“ com.fasterxml.jackson.databind.JsonMappingException: Jackson version is too old 2.3.1
  • .NET “底层”异步编程模式——异步编程模型(Asynchronous Programming Model,APM)...
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .net core 调用c dll_用C++生成一个简单的DLL文件VS2008
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .NET的数据绑定
  • @RestControllerAdvice异常统一处理类失效原因
  • [] 与 [[]], -gt 与 > 的比较
  • [ACM] hdu 1201 18岁生日
  • [android] 手机卫士黑名单功能(ListView优化)
  • [CF543A]/[CF544C]Writing Code
  • [na]wac无线控制器集中转发部署的几种情况
  • [SAP ABAP开发技术总结]面向对象OO
  • [SDOI2017]数字表格
  • [SV]SystemVerilog中指定打印格式
  • [Thinking]三个行