本文原创:huanghaijin

项目背景

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

生成视频可能的方案
  1. 纯前端的视频编码转换(例如WebM Encoder Whammy)

    • 图片地址只能是相对地址

    • 音乐不能收录

    • 生成的视频需要下载再上传

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

    • 截图多的时候,base64字符串形式的图片太大,在前端不好传给后端

    • 在前端截图还依赖用户电脑性能;

最后定的方案流程

  • canvas动画和截图在服务器端运行,后端根据标识获取截图

  • 利用FFmpeg将图片合并成视频,并将视频存储在server端,并返回相应下载url

  • 前端通过请求得到视频文件

前端canvas如何截图

每帧图片生成

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

function generatePng() {var canvas = document.createElement('canvas');let icavas = '#canvas' //渲染动画的canvas idif (wrapWidth == 2) {icavas = '#verticalCanvas'}var canvasNode = document.querySelector(icavas)canvas.width = canvasNode.width;canvas.height = canvasNode.height;var ctx = canvas.getContext('2d');ctx.drawImage(canvasNode, 0, 0);var imgData = canvas.toDataURL("image/png");return imgData;
}
复制代码

canvas动画截图的方法

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

setInterval(function() {imgsTemp.push(generatePng())
}, 1000/60)
复制代码

后端如何获取每帧图片

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

  • 最初设想:

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

  • 试运行方案:

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

const pages = {imageZoomOut: import ('./image_zoom_inout.js'), //缩放imageArt: import ('./image_art.js'), //擦除imageGrid: import ('./image_grid.js'), //网格imageRotate: import ('./image_rotate.js'), //开合imageFlash: import ('./image_flash.js'), //图文快闪imageVerticalArt: import ('./image_vertical_art.js'), //竖版擦除imageVerticalGrid: import ('./image_vertical_grid.js'), //竖版网格imageVerticalRotate: import ('./image_vertical_rotate.js'), //竖版开合imageVerticalFlash: import ('./image_vertical_flash.js'), //竖版图文快闪imageVerticalZoomOut: import ('./image_vertical_zoom_inout.js'), //竖版缩放imageVertical: import ('./image_vertical.js'), //竖版通用
};
var isShow = false
var imgsBase64 = []
var imgsTemp = []
var cutInter = null
var imgsTimeLong = 0
function getQuerys(tag) {let queryStr = window.location.search.slice(1);let queryArr = queryStr.split('&');let query = [];let spec = {}for (let i = 0, len = queryArr.length; i < len; i++) {let queryItem = queryArr[i].split('=');let qitem = decodeURIComponent(queryItem[1])if (queryItem[0] == tag) {query.push(qitem);} else {spec[queryItem[0]] = qitem}}return { list: query, spec: spec };
}
var getQuery = getQuerys('images')
var effectTag = getQuery.spec.tid
var wrapWidth = getQuery.spec.templateType
let num = 0
let imgArr = []
function creatImg() {var images = getQuery.listlet newImg = []let vh = wrapWidth == 1 ? 360 : 640let vw = wrapWidth == 1 ? 640 : 360if (effectTag.indexOf('Flash') > -1) {images.map(function(item, index) {if (11 === index || 13 === index || 16 === index) {var temp = new Image(vw, vh)temp.setAttribute('crossOrigin', 'anonymous');temp.src = item;newImg.push(temp)} else {newImg.push(item)}})imgArr = newImgrenderAnimate(effectTag)} else {images.map(function(item) {var temp = new Image(vw, vh)temp.setAttribute('crossOrigin', 'anonymous');temp.src = item;temp.onload = function() {num++if (num == images.length) {renderAnimate(effectTag)}}newImg.push(temp)})imgArr = newImg}
}
async function renderAnimate(page) {//await creatImg()let me = thisconst pageA = await pages[page];let oldDate = new Date().getTime()let icavas = '#canvas'if (wrapWidth == 2) {icavas = '#verticalCanvas'}let innerCanvas = document.querySelector(icavas)isShow = falsepageA[page].render(null, {canvas: innerCanvas,images: imgArr}, function() {//动画播完isShow = true;imgsTemp.push(generatePng())imgsBase64.push(imgsTemp)let now = new Date().getTime()window.imgsTimeLong = now - oldDateclearInterval(cutInter)document.getElementById('cutImg').innerHTML = 'done'//页面标识})cutInter = setInterval(function() {imgsTemp.push(generatePng())if (imgsTemp.length >= 50) {imgsBase64.push(imgsTemp)imgsTemp = []}}, 130)
}
function getImgs() {return imgsBase64
}
function generatePng() {var canvas = document.createElement('canvas');let icavas = '#canvas'if (wrapWidth == 2) {icavas = '#verticalCanvas'}var canvasNode = document.querySelector(icavas)canvas.width = canvasNode.width;canvas.height = canvasNode.height;var ctx = canvas.getContext('2d');ctx.drawImage(canvasNode, 0, 0);var imgData = canvas.toDataURL("image/png");return imgData;
}
window.imgsBase64 = imgsBase64 //截图存储变量creatImg()
复制代码

试运行方案的弊端:

  • 截图间隔130ms截一张图片,截图数量太少,导致生成的动画不流畅;

  • 截图间隔调成1秒60帧的话,动画播放缓慢,导致生成视频时间变长;(settimeout和setinterval的机制)

  • 图片尺寸在640x360或者360x640,生成的动画在手机端预览不清晰;

  • 需求换成图片尺寸为1280x720或者720x1280之后,原本15秒的动画在服务器端执行变成了70多秒

  • canvas截图存在跨域问题,可以如下设置

var temp = new Image(vw, vh)
temp.setAttribute('crossOrigin', 'anonymous');
复制代码

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

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

const {createCanvas,loadImage
} = require("canvas");
const pages = {imageZoomOut: require('./image_zoom_inout.js'), //缩放imageArt: require('./image_art.js'), //擦除imageGrid: require('./image_grid.js'), //网格imageRotate: require('./image_rotate.js'), //开合imageFlash: require('./image_flash.js'), //图文快闪imageVerticalArt: require('./image_vertical_art.js'), //竖版擦除imageVerticalGrid: require('./image_vertical_grid.js'), //竖版网格imageVerticalRotate: require('./image_vertical_rotate.js'), //竖版开合imageVerticalFlash: require('./image_vertical_flash.js'), //竖版图文快闪imageVerticalZoomOut: require('./image_vertical_zoom_inout.js'), //竖版缩放imageVertical: require('./image_vertical.js'), //竖版通用
};const fs = require("fs");
const querystring = require('querystring');
let args = process.argv && process.argv[2]
let parse = querystring.parse(args)let vh = parse.templateType == 1 ? 720 : 1280 //canvas 高
let vw = parse.templateType == 1 ? 1280 : 720 //canvas 宽
let imgSrcArray = parse.images //图片数组
let effectTag = parse.tid //动画效果let saveImgPath = process.argv && process.argv[3]let loadArr = []imgSrcArray.forEach(element => {if (/\.(jpg|jpeg|png|JPG|PNG)$/.test(element)) {loadArr.push(loadImage(element))} else {loadArr.push(element)}
});const canvas = createCanvas(vw, vh);
const ctx = canvas.getContext("2d");Promise.all(loadArr).then((images) => {//初始化动画console.log('开始动画')let oldDate = new Date().getTime()pages[effectTag].render(null, {canvas: canvas,images: images}, function() {clearInterval(interval)let now = new Date().getTime()console.log(now - oldDate, '动画结束')})const interval = setInterval((function() {let x = 0;return () => {x += 1;ctx.canvas.toDataURL('image/jpeg', function(err, png) {if (err) {console.log(err);return;}let data = png.replace(/^data:image\/\w+;base64,/, '');let buf = new Buffer(data, 'base64');fs.writeFile(`${saveImgPath}${x}.jpg`, buf, {}, (err) => {console.log(x, err);return;});});};})(),1000 / 60);}).catch(e => {console.log(e);});
复制代码

在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*12803) images是图片地址4)变量'./images/'是截图保存的地址,
复制代码

NODE环境下运行的弊端

  • 参数图片地址只能是相对地址
  • 动画过于复杂时,运行时间长,如下:当页面的图形数量达到一定时,动画每一帧就要大量调用canvas的API,要进行大量的计算,再加上图片体积很大,就会慢
每隔13秒循环一次下面的画图:for (var A = 0; 50 > A; A++)p.beginPath(),p.globalAlpha = 1 - A / 49,p.save(),p.arc(180,320,P + 2 * A, 0, 2 * Math.PI),p.clip(),p.drawImage(x[c], 0, 0, y.width, y.height),p.restore(),p.closePath();for (var S = 0; 50 > S; S++)p.beginPath(),p.globalAlpha = 1 - S / 49,p.save(),p.rect(0, 0, d + P + 2 * S, g + b + 2 * S),p.clip(),p.drawImage(x[c], 0, 0, y.width, y.height),p.restore(),p.closePath();
复制代码

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

后期优化的可能

  • 尝试用go语言,来截图;

  • 重写canvas动画;

番外

  • 视频码率

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

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

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

参考

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

segmentfault.com/a/119000000…

  • node-canvas API

github.com/Automattic/…

  • WebM Encoder Whammy

github.com/antimatter1…

转载于:https://juejin.im/post/5d024af36fb9a07f014ef134

前端canvas动画如何转成mp4视频相关推荐

  1. java mp4视频转换成h5_前端canvas动画如何转成mp4视频的方法

    用户通过上传合适尺寸的图片,选着渲染动画的效果和音乐,可以预览类似幻灯片的效果,最后点击确认生成视频,可以放到头条或者抖音播放. 生成视频可能的方案 纯前端的视频编码转换(例如WebM Encoder ...

  2. vep文件如何转换mp4_如何将m4v视频格式快速转换成mp4视频呢

    如今我们的数码相机,手机,摄像机越来越丰富了,拍摄的视频也越来越多了有的文件也是比较大,如何去转换为其它的格式呢还有就是如果你的视频制作完成后如何去转换为其它的格式放到网站上呢今天就给大家操作一下视频 ...

  3. 怎么把avi文件转换成mp4视频格式,4个高能方法

    怎么把avi文件转换成mp4视频格式? 当您下载到avi格式的视频文件时,您可能会选择将其转换为MP4格式的文件. avi是一种由微软开发的多媒体容器格式,尽管现在已经被认为是老旧的技术,但由于其简单 ...

  4. 【Python网络爬虫实战篇】使用selenium+requests爬取下载高清源视频:关于爬取m3u8文件链接解析为ts视频合并成mp4视频的分析实战

    这两天博主在摸鱼时,偶然间接触到了流媒体的概念,一时间来了兴致.再加上之前博主有着七.八年的视频制作经验,深知视频素材获取的不易.因此,打算利用自己所学的python网络爬虫的知识,通过编写代码实现获 ...

  5. 如何将m4v视频格式快速转换成mp4视频呢

    如今我们的数码相机,手机,摄像机越来越丰富了,拍摄的视频也越来越多了有的文件也是比较大,如何去转换为其它的格式呢还有就是如果你的视频制作完成后如何去转换为其它的格式放到网站上呢今天就给大家操作一下视频 ...

  6. 如何将AVI文件格式转换成MP4视频

    AVI格式是音视频交错格式,是Microsoft(即微软)推出作为Windows系统的多媒体格式.AVI格式是目前视频文件的主流格式,主要用于游戏录制,光盘文件,而且画质也不错,比较适合在电脑上观看: ...

  7. python视频格式转换_将ppt文件转成mp4视频的Python脚本

    ppt2mp4 (Python2.7) 将ppt文件转成mp4视频.GitHub 前提 1.需要Windows系统,并且启用了Windows多媒体播放器.需要安装Office 2010已上版本.因为P ...

  8. 工具---《.264视频 转成 MP4视频》

    <.264视频 转成 MP4视频> 安装了"爱奇艺万能播放器"可以打开.264视频,但是opencv却不能直接读取.264视频,还是需要想办法".264视频 ...

  9. 如何将AVI文件格式转换成MP4视频 1

    AVI格式是音视频交错格式,是Microsoft(即微软)推出作为Windows系统的多媒体格式.AVI格式是目前视频文件的主流格式,主要用于游戏录制,光盘文件,而且画质也不错,比较适合在电脑上观看: ...

最新文章

  1. Spring bean 之 FactoryBean
  2. Codeforces Round #200 (Div. 1)A. Rational Resistance 数学
  3. VTK:图表之ColorVerticesLookupTable
  4. CSharp设计模式读书笔记(10):装饰模式(学习难度:★★★☆☆,使用频率:★★★☆☆)...
  5. Python一直报错:SyntaxError: invalid syntax 的原因及解决办法
  6. 如何使用SQL Server Management Studio(SSMS)连接到Azure存储帐户
  7. Operations map 运营图谱
  8. 2018年wine QQ最完美解决方案(多Linux发行版通过测试并稳定运行)
  9. 《MATLAB智能算法超级学习手册》一一1.5 简单工程应用分析
  10. 计算机驱动空间的c盘不足怎么办,C盘磁盘空间不足怎么解决
  11. 非阻塞connect用法
  12. 大白菜u盘制作工具教程
  13. linux 如何删除gpt分区,Centos 7下如何删除GPT分区
  14. rss订阅 android,是的!我用这些软件订阅 RSS
  15. gfsj (logmein)
  16. 资讯汇总230207
  17. SW_DVD5_Office_Professional_Plus_2013_W32_ChnSimp_MLF_X18-55126
  18. RecyclerView源码剖析
  19. 数字信号处理:循环卷积快速计算技巧
  20. 可以直接用的Excel 宏定义-1

热门文章

  1. 客群分层怎区分,贷中风控来划分
  2. FME中的常用kml转换器介绍(一)
  3. 清华教授花一个月把Python所有基本用法归纳好了!末有惊喜
  4. python视频车流量计数_视频访问量实时统计项目学习
  5. 讽功能不实用 网友恶搞iWatch影片
  6. 认知控制和执行功能常用的实验范式(史上最全)
  7. 浏览器隐藏滚动条(不影响内容滚动)
  8. php检查链接是否有效,如何使用PHP编程检查有效(未死)链接?
  9. 今天动手打了女儿,但是她的行为却让我即感动又惭愧
  10. eclipse链接mysql数据池配置_Eclipse中配置Tomcat的数据库连接池 | 学步园