软件项目技术点(20)——导出视频

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(){……})

转载于:https://www.cnblogs.com/fangsmile/p/6283558.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值