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


Posted in HTML / CSS onJune 17, 2019

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

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

生成视频可能的方案

纯前端的视频编码转换(例如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张静止画面。

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

HTML / CSS 相关文章推荐
CSS3 新增选择器的实例
Nov 13 HTML / CSS
CSS3实现的渐变幻灯片效果
Dec 07 HTML / CSS
HTML5等待加载动画效果
Jul 27 HTML / CSS
canvas绘制表情包的示例代码
Jul 09 HTML / CSS
突破canvas语法限制 让他支持链式语法
Dec 24 HTML / CSS
html5中 media(播放器)的api使用指南
Dec 26 HTML / CSS
canvas与html5实现视频截图功能示例
Dec 15 HTML / CSS
HTML5地理定位_动力节点Java学院整理
Jul 12 HTML / CSS
Html5 canvas实现粒子时钟的示例代码
Sep 06 HTML / CSS
html5自定义video标签的海报与播放按钮功能
Dec 04 HTML / CSS
html5视频常用API接口的实战示例
Mar 20 HTML / CSS
使用HTML+Css+transform实现3D导航栏的示例代码
Mar 31 HTML / CSS
详解FireFox下Canvas使用图像合成绘制SVG的Bug
Jul 10 #HTML / CSS
canvas实现有递增动画的环形进度条的实现方法
Jul 10 #HTML / CSS
Html5新增标签与样式及让元素水平垂直居中
Jul 11 #HTML / CSS
解决H5的a标签的download属性下载service上的文件出现跨域问题
Jul 16 #HTML / CSS
html通过canvas转成base64的方法
Jul 18 #HTML / CSS
基于Canvas+Vue的弹幕组件的实现
Jul 23 #HTML / CSS
HTML5 WebGL 实现民航客机飞行监控系统
Jul 25 #HTML / CSS
You might like
php smarty模版引擎中变量操作符及使用方法
2009/12/11 PHP
CI框架数据库查询之join用法分析
2016/05/18 PHP
thinkphp5.1框架模板布局与模板继承用法分析
2019/07/19 PHP
firefox事件处理之自动查找event的函数(用于onclick=foo())
2010/08/05 Javascript
jQuery-serialize()输出序列化form表单值的方法
2012/12/26 Javascript
javascript创建和存储cookie示例
2014/01/07 Javascript
nodejs分页类代码分享
2014/06/17 NodeJs
jQuery中$.ajax()和$.getJson()同步处理详解
2015/08/12 Javascript
js移动焦点到最后位置的简单方法
2016/11/25 Javascript
利用forever和pm2部署node.js项目过程
2017/05/10 Javascript
浅谈struts1 &amp; jquery form 文件异步上传
2017/05/25 jQuery
Vue实现固定定位图标滑动隐藏效果
2019/05/30 Javascript
jquery分页优化操作实例分析
2019/08/23 jQuery
leaflet加载geojson叠加显示功能代码
2020/02/21 Javascript
jQuery实现动态加载瀑布流
2020/09/01 jQuery
js实现简单的随机点名器
2020/09/17 Javascript
基于Python的关键字监控及告警
2017/07/06 Python
Python 模拟购物车的实例讲解
2017/09/11 Python
PyQt5实现拖放功能
2018/04/25 Python
Python网页正文转换语音文件的操作方法
2018/12/09 Python
python argparser的具体使用
2019/11/10 Python
如何定义TensorFlow输入节点
2020/01/23 Python
python和js交互调用的方法
2020/06/23 Python
为什么说python更适合树莓派编程
2020/07/20 Python
CSS3实现闪烁动画效果的方法
2015/02/09 HTML / CSS
What's the difference between deep copy and shallow copy? (深拷贝与浅拷贝有什么区别)
2015/11/10 面试题
项目考察欢迎辞
2014/01/17 职场文书
爱国演讲稿400字
2014/05/07 职场文书
幼儿园健康教育方案
2014/06/14 职场文书
官僚主义现象查摆问题整改措施
2014/10/04 职场文书
六年级学生评语大全
2014/12/26 职场文书
党小组意见范文
2015/06/08 职场文书
英文投诉信格式
2015/07/03 职场文书
小学2016年第十八届推普周活动总结
2016/04/05 职场文书
如何使用Maxwell实时同步mysql数据
2021/04/08 MySQL
SpringBoot中使用Redis作为全局锁示例过程
2022/03/24 Java/Android