Canvas爲前端提供了動畫展現的平臺,隨着如今視頻娛樂的流行,你是否想過把Canvas動畫導出視頻?目前純前端的視頻編碼轉換(例如WebM Encoder Whammy)還存在許多限制,較爲成熟的方案是將每幀圖片傳給後端實現,由後端調用FFmpeg進行視頻轉碼。總體流程並不複雜,這篇文章將帶你們實現這個過程。前端
由前端記錄Canvas動畫的每幀圖像,以base64字符串形式傳給後端node
利用node fluent-ffmpeg模塊,調用FFmpeg將圖片合併成視頻,並將視頻存儲在server端,並返回相應下載urlgit
前端經過請求獲得視頻文件github
圖片生成能夠經過canvas原生接口toDataURL實現,最終返回base64形式的圖像數據。ajax
generatePng () { ... var imgData = canvas.toDataURL("image/png"); return imgData; }
動畫的記錄與傳送是個異步過程,這裏返回一個Promise,等待後端處理完畢,收到迴應後,即完成此異步過程。npm
如下代碼將canvas每幀動畫信息存入一個圖片數組imgs中,將數組轉成字符串的形式傳給後端。注意這裏contentType設置爲「text/plain」。canvas
generateVideo () { var that = this; return new Promise ( function (resolve, reject) { var imgs = []; ... window.requestAnimationFrame(that.recordTick.bind(that, imgs, resolve, reject)); } ) }
recordTick (imgs, resolve, reject) { ...//每幀動畫的記錄信息,如時間戳等 if (...) {//動畫終止條件 this.stopPlay(); imgs.push(this.generatePng()); $.ajax({ url: '/video/record', data: imgs.join(' '), method: 'POST', contentType: 'text/plain', success: function (data, textStatus, jqXHR) { resolve(data); }, error: function (jqXHR, textStatus, errorThrown) { reject(errorThrown); } }); } else { ...//每幀動畫展現的代碼 imgs.push(this.generatePng()); window.requestAnimationFrame(this.recordTick.bind(this, imgs, resolve, reject)); } }
上一節代碼中,動畫中止時,會經過post請求給後端傳送全部圖片數據,後端處理完畢後,返回數據中包含一個url,此url即爲視頻文件的下載地址。後端
爲了支持瀏覽器端用戶點擊下載,咱們須要用到a標籤的download屬性,此屬性能夠支持點擊a標籤後下載指定文件。數組
editor.generateVideo().then(function (data) { videoRecordingModal.setDownloadLink(data.url, data.filename); videoRecordingModal.changeStatus('recorded'); });
setDownloadLink: function (url, filename) { this.config.$dom.find('.video-download').attr('href', url); this.config.$dom.find('.video-download').attr('download', filename); }
接收到前端傳送的圖片數據後,咱們首先須要將圖片解析、存儲在服務器中,咱們創建以當前時間戳命名的文件夾,將圖片序列以必定格式存儲於其中。因爲每張圖片寫入都是異步過程,爲確保全部圖片都已處理完畢後,才執行視頻轉碼過程,咱們須要用到Promise.all。瀏覽器
Promise.all(imgs.map(function (value, index) { var img = decodeBase64Image(value) var data = img.data var type = img.type return new Promise(function (resolve, reject) { fs.writeFile(path.resolve(__dirname, (folder + '/img' + index + '.' + type)), data, 'base64', function(err) { if (err) { reject(err) } else { resolve() } }) }) })).then(function () { …//視頻轉碼 })
其中decodeBase64Image函數參考這裏。
視頻生成利用FFmpeg轉碼工具。
首先確保server端安裝了FFmpeg
brew install ffmpeg
在項目中安裝fluent-ffmpeg,這是node調用ffmpeg的接口模塊
npm install fluent-ffmpeg --save
結合上一節圖片序列存儲的代碼,整個接口代碼以下:
app.post('/video/record', function(req, res) { var imgs = req.text.split(' ') var timeStamp = Date.now() var folder = 'images/' + timeStamp if (!fs.existsSync(resolve(folder))){ fs.mkdirSync(resolve(folder)); } Promise.all(imgs.map(function (value, index) { var img = decodeBase64Image(value) var data = img.data var type = img.type return new Promise(function (resolve, reject) { fs.writeFile(path.resolve(__dirname, (folder + '/img' + index + '.' + type)), data, 'base64', function(err) { if (err) { reject(err) } else { resolve() } }) }) })).then(function () { var proc = new ffmpeg({ source: resolve(folder + '/img%d.png'), nolog: true }) .withFps(25) .on('end', function() { res.status(200) res.send({ url: '/video/mpeg/' + timeStamp, filename: 'jianshi' + timeStamp + '.mpeg' }) }) .on('error', function(err) { console.log('ERR: ' + err.message) }) .saveToFile(resolve('video/jianshi' + timeStamp + '.mpeg')) }) })
最終將視頻文件傳輸給前端的接口代碼以下:
app.get('/video/mpeg/:timeStamp', function(req, res) { res.contentType('mpeg'); var rstream = fs.createReadStream(resolve('video/jianshi' + req.params.timeStamp + '.mpeg')); rstream.pipe(res, {end: true}); })
注:此功能是我的項目」簡詩」的一部分,完整代碼能夠查看https://github.com/moyuer1992...