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

  • 相关阅读:
    Servlet 生命周期、工作原理(转)
    JVM的内存区域划分(转)
    Java的四个基本特性和对多态的理解
    单例模式和多例模式的区别(转)
    TCP/IP协议体系结构简介
    数据库优化性能
    存储过程的优缺点(转)
    ConurrentHashMap和Hashtable的区别
    XML和JSON优缺点
    HashMap和HashTable的区别
  • 原文地址:https://www.cnblogs.com/fangsmile/p/6283558.html
Copyright © 2020-2023  润新知