AxeSlide软件项目梳理 canvas绘图系列知识点整理
导出的视频和播放器自动播放效果时一样的,这样用户就可以传到视频网站分享出去,或者mp4文件发送分享给朋友。
导出视频过程
我们导出视频的思路就是:
将画布上绘制的画面一张张存储成图片,我们是一秒存20张图片,假如一个8帧的作品,每一帧的时间如下4+6+6+6+6+6+6+4=44(s),44s*20张/s=880张,我们导出这个视频一共就需要生成880张图片,生成图片成功后利用ffmpeg将图片生成视频。
如果作品里插入了背景音乐,我们需要将音频与视频合并成一个视频文件。
如果作品里有步序音乐,我们需要拆分成多个视频,再将这多个视频合并成一个。
如果作品有步序视频,那我们需要根据视频帧时间截取其中对应时间的视频,再将其与其他视频段合并。
基于这些需求我们就需要不断对作品中的音频和视频进行操作编辑。
多个视频合并成一个的前提条件是
1)每个视频是否含有音频须一致
2)每个视频的尺寸大小须一致
音频编辑API
我们定义了一个操作音频的专用类AudioEncoder,在其构造函数里我们创建一个FFmpeg命令
this.ffmpeg = require('fluent-ffmpeg');
this.encoder = new FfmpegCommand();
根据nodemodule的写法,我们也可以不用new操作符来使用构造函数。
this.ffmpeg = require('fluent-ffmpeg');
this.encoder = ffmpeg();
1 export class AudioEncoder { 2 private ffmpeg: any; 3 private encoder: any; 4 constructor(sourcePath: string) { 5 this.ffmpeg = require('fluent-ffmpeg'); 6 this.encoder = this.ffmpeg(sourcePath);//使用该初始化的encoder命令 7 } 8 //转换成mp3格式 9 toMP3(savePath: string, onComplete: Function, onError: Function) { 10 this.encoder.audioCodec('libmp3lame') 11 .on('end', () => { onComplete(); }) 12 .on('error', (err) => { onError(err); }) 13 .save(savePath); 14 } 15 //转换成只有音频信息的文件 16 convertAudio(savePath: string, onComplete: Function, onError: Function) { 17 this.encoder.noVideo().on('end', function () { 18 onComplete(); 19 }) 20 .on('error', function (err) { 21 onError(err); 22 }) 23 .save(savePath); 24 } 25 26 //生成一段只有一秒且无音量的音频文件 27 generateMusicNoAudio(savePath: string, onComplete: Function, onError: Function) { 28 this.encoder.audioFilters('volume=0').duration(1) 29 .on('end', function () { 30 onComplete(); 31 }) 32 .on('error', function (err) { 33 onError(err); 34 }) 35 .save(savePath); 36 } 37 }
视频编辑API
下面再列举几个用作视频转换合成的专用类VideoEncoder里面的方法:
1 /*合并图片成视频 2 targetPath:生成视频的路径 3 rate:帧率 4 onProgress:合成过程接受的函数,我们的软件有进度条 5 onComplete:合成完成且成功后的回调函数 6 */ 7 mergeImagesToVideo(targetPath: string, rate: number, onProgress: Function, onComplete: Function, onError: Function): void { 8 var that = this; 9 that.targetPath = targetPath; 10 that.onProgress = onProgress; 11 that.onComplete = onComplete; 12 that.onError = onError; 13 14 this.encoder.inputFPS(rate); 15 if (that.isHasAudio) 16 var videoSrc = that.tempVideoSrc; 17 else {//没有背景音乐 18 var videoSrc = targetPath; 19 } 20 21 this.encoder 22 .on('end', function () { 23 if (that.isHasAudio)//如果需要带有音频将视频再去合并一段音频 24 that.mergeVideoAudio(); 25 else 26 that.onComplete && that.onComplete(); 27 }) 28 .on('error', function (err) { 29 that.onError && that.onError(err); 30 }) 31 .on('progress', function (progress) { 32 var percentValue = progress.percent / 2 + 50; 33 that.onProgress && that.onProgress(percentValue); 34 }) 35 .save(videoSrc); 36 }
合并视频和音频前先去判断了音频的时间长度,再去调用mergeOneAudioVideo
1 //合并视频和音频前先去判断了音频的时间长度,再去调用mergeOneAudioVideo 2 mergeVideoAudio() { 3 var that = this; 4 //var audio = <HTMLAudioElement>document.getElementById("audio"); 5 var duration = 0.001; 6 //合并视频和音频前,以视频的时间长度为准,判断音频文件的时间长度是否够长,不够长的话将几个音频合成一个,扩展长度 7 FileSytem.ffmpeg.ffprobe(that.musicSrc, function (err, metadata) { 8 metadata.streams.forEach(function (obj, m) { 9 if (obj.codec_type == "audio") { 10 duration = obj.duration;//获取音频文件的时间长度 11 12 if (that.musicStartTime >= duration) 13 that.musicStartTime = 0; 14 if (duration - that.musicStartTime >= that.videoDuration) {//不用合成长音频,音频时间长度大于视频时间长度 15 that.mergeOneAudioVideo(that.musicSrc); 16 } else {//音频短 需要合并几个音频成一个 17 var count = Math.ceil((that.videoDuration + that.musicStartTime) / duration);//计算需要将几个音频合成一个 18 var musicsMerge = that.ffmpeg(that.musicSrc); 19 for (var i = 0; i < count - 1; i++) { 20 musicsMerge.input(that.musicSrc); 21 } 22 musicsMerge.noVideo() 23 .on('end', function () { 24 that.onProgress && that.onProgress(95); 25 //多个音频合成一个之后再将其与视频合成 26 that.mergeOneAudioVideo(that.tempMusicSrc); 27 }) 28 .on('error', function (err) { 29 that.onError && that.onError(err); 30 }) 31 .mergeToFile(that.tempMusicSrc); 32 } 33 } 34 }) 35 if (duration == 0.001) { 36 that.onError && that.onError("mergeVideoAudio 音频信息出错"); 37 } 38 }); 39 } 40 //将视频和音频合成一个视频 41 mergeOneAudioVideo(musicSrc) { 42 var that = this; 43 var proc = this.ffmpeg(this.tempVideoSrc);//图片合成的视频片段的路径 44 proc.input(musicSrc);//加入音频参数 45 proc.setStartTime(that.musicStartTime);//设置音频开始时间 46 if (that.isAudioMuted) {//判断是否该静音 47 proc.audioFilters('volume=0'); 48 } 49 proc.addOptions(['-shortest']);//以视频和音频中较短的为准 50 proc.on('end', function () { 51 FileSytem.remove(that.tempMusicSrc, null); 52 FileSytem.remove(that.tempVideoSrc, null); 53 that.onProgress && that.onProgress(100); 54 that.onComplete && that.onComplete(); 55 }).on('error', function (err) { 56 that.onError && that.onError(err); 57 }).save(that.targetPath); 58 }
将多个视频合成一个
1 //将多个视频合成一个 2 mergeVideos(paths: any, savePath: string, onProgress: Function, onComplete: Function, onError: Function) { 3 var count = paths.length; 4 for (var i = 1; i < count; i++) { 5 this.encoder.input(paths[i]); 6 } 7 this.encoder 8 .on('end', function () { 9 onComplete(); 10 }) 11 .on('error', function (err) { 12 onError(err); 13 }) 14 .on('progress', function (progress) { 15 //console.log(progress); 16 var percentValue = Math.round(progress.percent); 17 onProgress && onProgress(percentValue); 18 }) 19 .mergeToFile(savePath); 20 }
改变视频尺寸
为保证插入视频(并且是视频帧)的作品能导出成功,我们可能需要改变插入视频的尺寸。
例如我们插入一个原尺寸是960*400的视频,导出视频尺寸为640*480,我们不能拉伸视频,位置要居中:
使用cmd命令执行改变视频尺寸的命令行:
1 if (ratio_i > ratio3) {//ratio_i插入视频的宽高比,ratio3导出视频尺寸的宽高比 2 w = that.videoWidth;//that.videoWidth导出视频的宽,that.videoHeight导出视频的高 3 h = parseInt(that.videoWidth / ratio_i); 4 x = 0; 5 y = (that.videoHeight - h) / 2; 6 } else { 7 w = parseInt(that.videoHeight * ratio_i); 8 h = that.videoHeight; 9 x = (that.videoWidth - w) / 2; 10 y = 0; 11 } 12 that.childProcessObj = childProcess.exec(ffmpeg + " -i " + (<Core.Video>that.currentFrame.element).src + " -aspect " + ratio + " -s " + that.videoWidth + "x" + that.videoHeight + " -vf scale=w=" + w + ":h=" + h + ",pad=w=" + that.videoWidth + ":h=" + that.videoHeight + ":x=" + x + ":y=" + y + ":color=black" + " -t " + frame.actualDuration + " -ss " + startTime + (returnData["channels"] > 2 ? " -ac 2 " : " ") + partVideoPath); 13 that.childProcessObj && that.childProcessObj.on("exit", function (e) { 14 if (!that.isClickCancel) { 15 that.videoPartPaths.push(partVideoPath); 16 that.isNext = true; 17 callback && callback(); 18 } 19 that.childProcessObj = null; 20 }).on("error", function (e) { 21 that.onExportVideoComplete(); 22 Common.Logger.setOpeLog(1003, "文件:ExportVideo,方法:startPlayOnePart,异常信息:" + e); 23 that.callBack && that.callBack(false, e); 24 that.childProcessObj = null; 25 })
我们通过调试来监视实际执行的命令:
"ffmpeg -i slideview/work/image/video_4y8spsLLG.mp4 -aspect 4:3 -s 640x480 -vf scale=w=640:h=266,pad=w=640:h=480:x=0:y=107:color=black -t 46.613333 -ss 0 slideview/work/video/EkeDRDd88M/videoFrame0.mp4"
注意:按原尺寸960:400=640:266 保证不拉伸
x=0:y=107((480-266)/2=107) 保证视频时居中的
color=black 空白填充色
取消视频导出
当我们导出视频到中间时,如果不想继续导出点击进度条的取消会调用this.encoder.kill()
结束掉命令,这个函数执行后会触发on("error",function(){……})