经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » HTML/CSS » HTML5 » 查看文章
前端canvas动画如何转成mp4视频的方法
来源:jb51  时间:2019/6/18 8:38:18  对本文有异议

用户通过上传合适尺寸的图片,选着渲染动画的效果和音乐,可以预览类似幻灯片的效果,最后点击确认生成视频,可以放到头条或者抖音播放。

生成视频可能的方案

纯前端的视频编码转换(例如WebM Encoder Whammy)

  • 图片地址只能是相对地址
  • 音乐不能收录
  • 生成的视频需要下载再上传

将每帧图片传给后端实现,由后端调用FFmpeg进行视频转码

  • 截图多的时候,base64字符串形式的图片太大,在前端不好传给后端
  • 在前端截图还依赖用户电脑性能;

最后定的方案流程

  • canvas动画和截图在服务器端运行,后端根据标识获取截图
  • 利用FFmpeg将图片合并成视频,并将视频存储在server端,并返回相应下载url
  • 前端通过请求得到视频文件

前端canvas如何截图

每帧图片生成

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

  1. function generatePng() {
  2. var canvas = document.createElement('canvas');
  3. let icavas = '#canvas' //渲染动画的canvas id
  4. if (wrapWidth == 2) {
  5. icavas = '#verticalCanvas'
  6. }
  7. var canvasNode = document.querySelector(icavas)
  8.  
  9. canvas.width = canvasNode.width;
  10. canvas.height = canvasNode.height;
  11. var ctx = canvas.getContext('2d');
  12. ctx.drawImage(canvasNode, 0, 0);
  13. var imgData = canvas.toDataURL("image/png");
  14. return imgData;
  15. }

canvas动画截图的方法

用setInterval定时执行图片生成的方法,当然也可以用requestAnimationFrame

  1. setInterval(function() {
  2. imgsTemp.push(generatePng())
  3. }, 1000/60)

后端如何获取每帧图片

方案一:无头浏览器运行前端canvas动画js,然后js截图

最初设想:

截图用console.log打印出来,canvas截图是base64格式的,一个15秒的动画,截图有100多张,直接导致服务器运行崩溃(被否了);

试运行方案:

截图存储在js变量中,动画播放完成,在页面中加一个标识,然后后端去取这个变量,代码如下:

  1. const pages = {
  2. imageZoomOut: import ('./image_zoom_inout.js'), //缩放
  3. imageArt: import ('./image_art.js'), //擦除
  4. imageGrid: import ('./image_grid.js'), //网格
  5. imageRotate: import ('./image_rotate.js'), //开合
  6. imageFlash: import ('./image_flash.js'), //图文快闪
  7. imageVerticalArt: import ('./image_vertical_art.js'), //竖版擦除
  8. imageVerticalGrid: import ('./image_vertical_grid.js'), //竖版网格
  9. imageVerticalRotate: import ('./image_vertical_rotate.js'), //竖版开合
  10. imageVerticalFlash: import ('./image_vertical_flash.js'), //竖版图文快闪
  11. imageVerticalZoomOut: import ('./image_vertical_zoom_inout.js'), //竖版缩放
  12. imageVertical: import ('./image_vertical.js'), //竖版通用
  13. };
  14. var isShow = false
  15. var imgsBase64 = []
  16. var imgsTemp = []
  17. var cutInter = null
  18. var imgsTimeLong = 0
  19. function getQuerys(tag) {
  20. let queryStr = window.location.search.slice(1);
  21. let queryArr = queryStr.split('&');
  22. let query = [];
  23. let spec = {}
  24. for (let i = 0, len = queryArr.length; i < len; i++) {
  25. let queryItem = queryArr[i].split('=');
  26. let qitem = decodeURIComponent(queryItem[1])
  27. if (queryItem[0] == tag) {
  28. query.push(qitem);
  29. } else {
  30. spec[queryItem[0]] = qitem
  31. }
  32. }
  33. return { list: query, spec: spec };
  34. }
  35. var getQuery = getQuerys('images')
  36. var effectTag = getQuery.spec.tid
  37. var wrapWidth = getQuery.spec.templateType
  38. let num = 0
  39. let imgArr = []
  40. function creatImg() {
  41. var images = getQuery.list
  42. let newImg = []
  43. let vh = wrapWidth == 1 ? 360 : 640
  44. let vw = wrapWidth == 1 ? 640 : 360
  45. if (effectTag.indexOf('Flash') > -1) {
  46. images.map(function(item, index) {
  47. if (11 === index || 13 === index || 16 === index) {
  48. var temp = new Image(vw, vh)
  49. temp.setAttribute('crossOrigin', 'anonymous');
  50. temp.src = item;
  51. newImg.push(temp)
  52.  
  53. } else {
  54. newImg.push(item)
  55. }
  56. })
  57. imgArr = newImg
  58. renderAnimate(effectTag)
  59. } else {
  60. images.map(function(item) {
  61. var temp = new Image(vw, vh)
  62. temp.setAttribute('crossOrigin', 'anonymous');
  63. temp.src = item;
  64. temp.onload = function() {
  65. num++
  66. if (num == images.length) {
  67. renderAnimate(effectTag)
  68. }
  69. }
  70. newImg.push(temp)
  71. })
  72. imgArr = newImg
  73. }
  74. }
  75. async function renderAnimate(page) {
  76. //await creatImg()
  77. let me = this
  78. const pageA = await pages[page];
  79. let oldDate = new Date().getTime()
  80. let icavas = '#canvas'
  81. if (wrapWidth == 2) {
  82. icavas = '#verticalCanvas'
  83. }
  84. let innerCanvas = document.querySelector(icavas)
  85. isShow = false
  86. pageA[page].render(null, {
  87. canvas: innerCanvas,
  88. images: imgArr
  89. }, function() {
  90. //动画播完
  91. isShow = true;
  92. imgsTemp.push(generatePng())
  93. imgsBase64.push(imgsTemp)
  94. let now = new Date().getTime()
  95. window.imgsTimeLong = now - oldDate
  96.  
  97. clearInterval(cutInter)
  98. document.getElementById('cutImg').innerHTML = 'done'//页面标识
  99. })
  100. cutInter = setInterval(function() {
  101. imgsTemp.push(generatePng())
  102. if (imgsTemp.length >= 50) {
  103. imgsBase64.push(imgsTemp)
  104. imgsTemp = []
  105. }
  106. }, 130)
  107. }
  108. function getImgs() {
  109. return imgsBase64
  110. }
  111. function generatePng() {
  112. var canvas = document.createElement('canvas');
  113. let icavas = '#canvas'
  114. if (wrapWidth == 2) {
  115. icavas = '#verticalCanvas'
  116. }
  117.  
  118. var canvasNode = document.querySelector(icavas)
  119. canvas.width = canvasNode.width;
  120. canvas.height = canvasNode.height;
  121. var ctx = canvas.getContext('2d');
  122. ctx.drawImage(canvasNode, 0, 0);
  123. var imgData = canvas.toDataURL("image/png");
  124. return imgData;
  125. }
  126. window.imgsBase64 = imgsBase64 //截图存储变量
  127.  
  128. creatImg()

试运行方案的弊端:

  • 截图间隔130ms截一张图片,截图数量太少,导致生成的动画不流畅;
  • 截图间隔调成1秒60帧的话,动画播放缓慢,导致生成视频时间变长;(settimeout和setinterval的机制)
  • 图片尺寸在640x360或者360x640,生成的动画在手机端预览不清晰;
  • 需求换成图片尺寸为1280x720或者720x1280之后,原本15秒的动画在服务器端执行变成了70多秒
  • canvas截图存在跨域问题,可以如下设置
  1. var temp = new Image(vw, vh)
  2. temp.setAttribute('crossOrigin', 'anonymous');

最终方案:在NODE端运行动画

用node-canvas,把每帧截图用 fs.writeFile 写到指定的文件夹里

  1. const {
  2. createCanvas,
  3. loadImage
  4. } = require("canvas");
  5. const pages = {
  6. imageZoomOut: require('./image_zoom_inout.js'), //缩放
  7. imageArt: require('./image_art.js'), //擦除
  8. imageGrid: require('./image_grid.js'), //网格
  9. imageRotate: require('./image_rotate.js'), //开合
  10. imageFlash: require('./image_flash.js'), //图文快闪
  11. imageVerticalArt: require('./image_vertical_art.js'), //竖版擦除
  12. imageVerticalGrid: require('./image_vertical_grid.js'), //竖版网格
  13. imageVerticalRotate: require('./image_vertical_rotate.js'), //竖版开合
  14. imageVerticalFlash: require('./image_vertical_flash.js'), //竖版图文快闪
  15. imageVerticalZoomOut: require('./image_vertical_zoom_inout.js'), //竖版缩放
  16. imageVertical: require('./image_vertical.js'), //竖版通用
  17. };
  18.  
  19. const fs = require("fs");
  20. const querystring = require('querystring');
  21. let args = process.argv && process.argv[2]
  22. let parse = querystring.parse(args)
  23.  
  24. let vh = parse.templateType == 1 ? 720 : 1280 //canvas 高
  25. let vw = parse.templateType == 1 ? 1280 : 720 //canvas 宽
  26. let imgSrcArray = parse.images //图片数组
  27. let effectTag = parse.tid //动画效果
  28.  
  29. let saveImgPath = process.argv && process.argv[3]
  30.  
  31. let loadArr = []
  32.  
  33. imgSrcArray.forEach(element => {
  34. if (/\.(jpg|jpeg|png|JPG|PNG)$/.test(element)) {
  35. loadArr.push(loadImage(element))
  36. } else {
  37. loadArr.push(element)
  38. }
  39. });
  40.  
  41. const canvas = createCanvas(vw, vh);
  42. const ctx = canvas.getContext("2d");
  43.  
  44. Promise.all(loadArr)
  45. .then((images) => {
  46. //初始化动画
  47. console.log('开始动画')
  48. let oldDate = new Date().getTime()
  49. pages[effectTag].render(null, {
  50. canvas: canvas,
  51. images: images
  52. }, function() {
  53. clearInterval(interval)
  54. let now = new Date().getTime()
  55. console.log(now - oldDate, '动画结束')
  56. })
  57.  
  58. const interval = setInterval(
  59. (function() {
  60. let x = 0;
  61. return () => {
  62. x += 1;
  63. ctx.canvas.toDataURL('image/jpeg', function(err, png) {
  64. if (err) {
  65. console.log(err);
  66. return;
  67. }
  68. let data = png.replace(/^data:image\/\w+;base64,/, '');
  69. let buf = new Buffer(data, 'base64');
  70. fs.writeFile(`${saveImgPath}${x}.jpg`, buf, {}, (err) => {
  71. console.log(x, err);
  72. return;
  73. });
  74. });
  75. };
  76. })(),
  77. 1000 / 60
  78. );
  79. })
  80. .catch(e => {
  81. console.log(e);
  82. });

在iterm下执行下面命令

node testCanvas.js 'tid=imageArt&templateType=1&images=../assets/imgs/8.png&images=../assets/imgs/6.png&images=../assets/imgs/7.png&images=../assets/imgs/6.png&images=../assets/imgs/8.png&images=../assets/imgs/7.png&images=../assets/imgs/4.png&images=../assets/imgs/6.png&images=../assets/imgs/8.png&images=../assets/imgs/7.png' './images/'

参数说明:
1)tid 是动画名称
2)templateType是尺寸:"1":1280*720;"2":720*1280
3) images是图片地址
4)变量'./images/'是截图保存的地址,

NODE环境下运行的弊端

  • 参数图片地址只能是相对地址
  • 动画过于复杂时,运行时间长,如下:当页面的图形数量达到一定时,动画每一帧就要大量调用canvas的API,要进行大量的计算,再加上图片体积很大,就会慢

每隔13秒循环一次下面的画图:   

 

  1. for (var A = 0; 50 > A; A++)
  2. p.beginPath(),
  3. p.globalAlpha = 1 - A / 49,
  4. p.save(),
  5. p.arc(180,320,P + 2 * A, 0, 2 * Math.PI),
  6. p.clip(),
  7. p.drawImage(x[c], 0, 0, y.width, y.height),
  8. p.restore(),
  9. p.closePath();
  10.  
  11. for (var S = 0; 50 > S; S++)
  12. p.beginPath(),
  13. p.globalAlpha = 1 - S / 49,
  14. p.save(),
  15. p.rect(0, 0, d + P + 2 * S, g + b + 2 * S),
  16. p.clip(),
  17. p.drawImage(x[c], 0, 0, y.width, y.height),
  18. p.restore(),
  19. p.closePath();

因为Node.js 的事件循环模型,要求 Node.js 的使用必须时刻保证 Node.js 的循环能够运转,如果出现非常耗时的函数,那么事件循环就会陷入进去,无法及时处理其他的任务,所以导致有些动画还是慢

后期优化的可能

尝试用go语言,来截图;

重写canvas动画;

番外

视频码率

视频码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是kbps即千位每秒。通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件。举例来看,对于一个音频,其码率越高,被压缩的比例越小,音质损失越小,与音源的音质越接近。

FPS 每秒传输帧数(Frames Per Second))

FPS是图像领域中的定义,是指画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。FPS是测量用于保存、显示动态视频的信息数量。每秒钟帧数愈多,所显示的动作就会愈流畅。通常,要避免动作不流畅的最低是30。例如电影以每秒24张画面的速度播放,也就是一秒钟内在屏幕上连续投射出24张静止画面。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号