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

posted @ 2017-01-17 15:21  方帅  阅读(730)  评论(0编辑  收藏  举报