软件项目技术点(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(){……})