介绍Canvas为前端提供了一个动画展示的平台。随着视频娱乐的普及,你有没有想过导出Canvas动画视频?目前纯前端视频编码转换(如WebMEncoderWhammy)还有很多局限性。比较成熟的方案是将每一帧图片传给后端实现,后端调用FFmpeg进行视频转码。整体流程并不复杂,本文将带您走完整个流程。整体方案是在前端记录每一帧Canvas动画,以base64字符串的形式发送给后端。使用nodefluent-ffmpeg模块,调用FFmpeg将图片合并成视频,将视频存储在服务器端,返回相应的下载url前端通过请求视频文件的前端部分生成每一帧图片,可以通过canvas原生接口生成图片到DataURL,最后以base64的形式返回图片数据。generatePng(){...varimgData=canvas.toDataURL("image/png");returnimgData;}动画录制和图片流动画的录制和传输是一个异步过程,这里返回一个Promise,等待后端完成processing,andreceives一旦收到响应,这个异步过程就完成了。下面的代码将画布每一帧的动画信息存储到一个图片数组imgs中,并将该数组转成字符串发送给后端。请注意,这里的contentType设置为“text/plain”。generateVideo(){varthat=this;returnnewPromise(function(resolve,reject){varimgs=[];...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));}}VideoDownload上一段代码中,当动画停止时,会通过post请求将所有图片数据发送到后端。结束处理完成后,返回数据中包含一个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){varimg=decodeBase64Image(value)vardata=img.datavartype=img.typereturnnewPromise(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转码工具。首先,确保在服务器端安装了FFmpegbrewinstallffmpeg。在项目中安装fluent-ffmpeg。这是接口模块npminstallfluent-ffmpeg--保存节点调用ffmpeg。结合上一节存储图像序列的代码,整个接口代码如下:app.post('/video/record',function(req,res){varimgs=req.text.split('')vartimeStamp=Date.now()varfolder='images/'+timeStampif(!fs.existsSync(resolve(folder))){fs.mkdirSync(resolve(folder));}Promise.all(imgs.map(function(值,索引){varimg=decodeBase64Image(值)vardata=img.datavartype=img.typereturnnewPromise(函数(解析,拒绝){fs.writeFile(path.resolve(__dirname,(文件夹+'/img'+index+'。'+type)),data,'base64',function(err){if(err){reject(err)}else{resolve()}})})})).then(function(){varproc=newffmpeg({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'))})})视频下载最终保存视频的接口代码为文件传输到前端如下:get('/video/mpeg/:timeStamp',function(req,res){res.contentType('mpeg');varrstream=fs.createReadStream(resolve('video/jianshi'+req.params.timeStamp+'.mpeg'));rstream.pipe(res,{end:true});})效果预览注:该函数是个人项目“简氏”的一部分,完整代码可以查看https://github.com/魔鱼儿1992...
