前端canvas动画如何转成mp4视频

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

前端canvas动画如何转成mp4视频_第1张图片

生成视频可能的方案

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

图片地址只能是相对地址

音乐不能收录

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

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

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

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

最后定的方案流程

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

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

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

前端canvas如何截图

每帧图片生成

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

functiongeneratePng(){varcanvas =document.createElement('canvas');leticavas ='#canvas'//渲染动画的canvas idif(wrapWidth ==2) {        icavas ='#verticalCanvas'}varcanvasNode =document.querySelector(icavas)    canvas.width = canvasNode.width;    canvas.height = canvasNode.height;varctx = canvas.getContext('2d');    ctx.drawImage(canvasNode,0,0);varimgData = canvas.toDataURL("image/png");returnimgData;}复制代码

canvas动画截图的方法

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

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

后端如何获取每帧图片

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

最初设想:

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

试运行方案:

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

constpages = {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'),//竖版通用};varisShow =falsevarimgsBase64 = []varimgsTemp = []varcutInter =nullvarimgsTimeLong =0functiongetQuerys(tag){letqueryStr =window.location.search.slice(1);letqueryArr = queryStr.split('&');letquery = [];letspec = {}for(leti =0, len = queryArr.length; i < len; i++) {letqueryItem = queryArr[i].split('=');letqitem =decodeURIComponent(queryItem[1])if(queryItem[0] == tag) {            query.push(qitem);        }else{            spec[queryItem[0]] = qitem        }    }return{list: query,spec: spec };}vargetQuery = getQuerys('images')vareffectTag = getQuery.spec.tidvarwrapWidth = getQuery.spec.templateTypeletnum =0letimgArr = []functioncreatImg(){varimages = getQuery.listletnewImg = []letvh = wrapWidth ==1?360:640letvw = wrapWidth ==1?640:360if(effectTag.indexOf('Flash') >-1) {        images.map(function(item, index){if(11=== index ||13=== index ||16=== index) {vartemp =newImage(vw, vh)                temp.setAttribute('crossOrigin','anonymous');                temp.src = item;                newImg.push(temp)            }else{                newImg.push(item)            }        })        imgArr = newImg        renderAnimate(effectTag)    }else{        images.map(function(item){vartemp =newImage(vw, vh)            temp.setAttribute('crossOrigin','anonymous');            temp.src = item;            temp.onload =function(){                num++if(num == images.length) {                    renderAnimate(effectTag)                }            }            newImg.push(temp)        })        imgArr = newImg    }}asyncfunctionrenderAnimate(page){//await creatImg()letme =thisconstpageA =awaitpages[page];letoldDate =newDate().getTime()leticavas ='#canvas'if(wrapWidth ==2) {        icavas ='#verticalCanvas'}letinnerCanvas =document.querySelector(icavas)    isShow =falsepageA[page].render(null, {canvas: innerCanvas,images: imgArr    },function(){//动画播完isShow =true;        imgsTemp.push(generatePng())        imgsBase64.push(imgsTemp)letnow =newDate().getTime()window.imgsTimeLong = now - oldDate        clearInterval(cutInter)document.getElementById('cutImg').innerHTML ='done'//页面标识})    cutInter = setInterval(function(){        imgsTemp.push(generatePng())if(imgsTemp.length >=50) {            imgsBase64.push(imgsTemp)            imgsTemp = []        }    },130)}functiongetImgs(){returnimgsBase64}functiongeneratePng(){varcanvas =document.createElement('canvas');leticavas ='#canvas'if(wrapWidth ==2) {        icavas ='#verticalCanvas'}varcanvasNode =document.querySelector(icavas)    canvas.width = canvasNode.width;    canvas.height = canvasNode.height;varctx = canvas.getContext('2d');    ctx.drawImage(canvasNode,0,0);varimgData = canvas.toDataURL("image/png");returnimgData;}window.imgsBase64 = imgsBase64//截图存储变量creatImg()复制代码

试运行方案的弊端:

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

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

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

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

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

vartemp =newImage(vw, vh)temp.setAttribute('crossOrigin','anonymous');复制代码

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

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

const{    createCanvas,    loadImage} =require("canvas");constpages = {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'),//竖版通用};constfs =require("fs");constquerystring =require('querystring');letargs = process.argv && process.argv[2]letparse = querystring.parse(args)letvh = parse.templateType ==1?720:1280//canvas 高letvw = parse.templateType ==1?1280:720//canvas 宽letimgSrcArray = parse.images//图片数组leteffectTag = parse.tid//动画效果letsaveImgPath = process.argv && process.argv[3]letloadArr = []imgSrcArray.forEach(element=>{if(/\.(jpg|jpeg|png|JPG|PNG)$/.test(element)) {        loadArr.push(loadImage(element))    }else{        loadArr.push(element)    }});constcanvas = createCanvas(vw, vh);constctx = canvas.getContext("2d");Promise.all(loadArr)    .then((images) =>{//初始化动画console.log('开始动画')letoldDate =newDate().getTime()        pages[effectTag].render(null, {canvas: canvas,images: images        },function(){            clearInterval(interval)letnow =newDate().getTime()console.log(now - oldDate,'动画结束')        })constinterval = setInterval(            (function(){letx =0;return()=>{                    x +=1;                    ctx.canvas.toDataURL('image/jpeg',function(err, png){if(err) {console.log(err);return;                        }letdata = png.replace(/^data:image\/\w+;base64,/,'');letbuf =newBuffer(data,'base64');                        fs.writeFile(`${saveImgPath}${x}.jpg`, buf, {}, (err) => {console.log(x, err);return;                        });                    });                };            })(),1000/60);    })    .catch(e=>{console.log(e);    });复制代码

在iterm下执行下面命令

nodetestCanvas.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(varA=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(varS=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张静止画面。

你可能感兴趣的:(前端canvas动画如何转成mp4视频)