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

生成视频可能的方案
纯前端的视频编码转换(例如WebM Encoder Whammy)
- 图片地址只能是相对地址
- 音乐不能收录
- 生成的视频需要下载再上传
将每帧图片传给后端实现,由后端调用FFmpeg进行视频转码
- 截图多的时候,base64字符串形式的图片太大,在前端不好传给后端
- 在前端截图还依赖用户电脑性能;
最后定的方案流程
- canvas动画和截图在服务器端运行,后端根据标识获取截图
- 利用FFmpeg将图片合并成视频,并将视频存储在server端,并返回相应下载url
- 前端通过请求得到视频文件
前端canvas如何截图
每帧图片生成
图片生成可以通过canvas原生接口toDataURL实现,最终返回base64形式的图像数据
- function generatePng() {
- var canvas = document.createElement('canvas');
- let icavas = '#canvas' //渲染动画的canvas id
- 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;
- }
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.list
- let newImg = []
- let vh = wrapWidth == 1 ? 360 : 640
- let vw = wrapWidth == 1 ? 640 : 360
- if (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 = newImg
- renderAnimate(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 = this
- const pageA = await pages[page];
- let oldDate = new Date().getTime()
- let icavas = '#canvas'
- if (wrapWidth == 2) {
- icavas = '#verticalCanvas'
- }
- let innerCanvas = document.querySelector(icavas)
- isShow = false
- pageA[page].render(null, {
- canvas: innerCanvas,
- images: imgArr
- }, function() {
- //动画播完
- isShow = true;
- imgsTemp.push(generatePng())
- imgsBase64.push(imgsTemp)
- let now = new Date().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)
- }
- 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*1280
3) 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张静止画面。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。