前端canvas動畫如何轉成mp4視頻

本文原創:huanghaijin前端

項目背景

用戶經過上傳合適尺寸的圖片,選着渲染動畫的效果和音樂,能夠預覽相似幻燈片的效果,最後點擊確認生成視頻,能夠放到頭條或者抖音播放。node

cv2.jpg

生成視頻可能的方案
  1. 純前端的視頻編碼轉換(例如WebM Encoder Whammy)git

    • 圖片地址只能是相對地址github

    • 音樂不能收錄canvas

    • 生成的視頻須要下載再上傳segmentfault

  2. 將每幀圖片傳給後端實現,由後端調用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張靜止畫面。

參考

  • Node + FFmpeg 實現Canvas動畫導出視頻

segmentfault.com/a/119000000…

  • node-canvas API

github.com/Automattic/…

  • WebM Encoder Whammy

github.com/antimatter1…

相關文章
相關標籤/搜索