
/*=============================================================================================
    author:ZhaoJun
    version:1.2
    email:2732810281@qq.com
=============================================================================================*/


//严格模式
/*-------------------------------------------------------------------------------------------*/ 
'use strict';
/*-------------------------------------------------------------------------------------------*/
 
//预处理
/*-------------------------------------------------------------------------------------------*/ 
//yuv文件
//ffmpeg -i input.mp4 -an -c:v rawvideo -pixel_format yuv420p out.yuv

//YUV，分为三个分量，“Y”表示明亮度（Luminance或Luma）
//也就是灰度值；而“U”和“V” 表示的则是色度（Chrominance或Chroma），作用是描述影像色彩及饱和度，用于指定像素的颜色。 


//视频处理生成后的宽高必须为偶数

//全局变量
//in_w/iw in_h/ih   文件原始宽高
//out_w/ow out_h/oh 
//iw、ih、ow、oh，分别表示输入宽、输入高、输出宽、输出高，假设要缩小到原来的1/2，则可以用scale=iw/2:ih/2
//w,h 代表覆盖层的宽高
//tw th 字的宽高
//假设要宽度变成原来的2/3，高度和最后的宽度一样，则可以用scale=2/3*iw:ow，生成后的宽高必须为偶数（trunc(iw/2)*2）
//t 当前时间秒
//TB 总时间
//n表示第几帧
//

//%3d 3（3可改）位长的数字自增，比如可用于提取/写入多张帧图片
//"overlay=x=if(gte(t\,2)\,((t-2)*80)-w\,NAN) 如果当前时间大于2s 返回(t-2)*80)-w"，否则返回NAN
//"overlay=x=if(lt(gte(t\,20)\,10)\,10\,NAN ):y=10,overlay=x=if(gt(mod(t\,20)\,10)\,W-w-10\,NAN ) :y=10" 每隔10秒交替出现
//lt gte mod if  等操作符下的成员需要加\,(不知道规则，就成员后面都加)


//PTS,每一个视频帧都有一个带有时间戳的头部，两个连续的帧之间的时差是1/fps。比如帧率fps为25，则时差为1/25=0.04秒，
//这个值就是PTS。如果想改变播放速度，就是改变PTS的值了。PTS主要用于视频的同步和输出.


//从第2s到第12s内，每秒截取1帧图像:
//ffmpeg -y -ss 2 -i test.mp4 -r 1 -t 10 -f image2 -s 352x240 img%03d.png


//map用法
// 倒序排列输入流
// Stream #0:0(ger): Subtitle: text (default)
// Stream #0:1(eng): Audio: dts (DTS), 48000 Hz, 5.1(side), s16, 1536 kb/s
// Stream #0:2(ger): Audio: dts (DTS), 48000 Hz, 5.1(side), s16, 1536 kb/s (default)
// Stream #0:3(eng): Video: h264 (High), yuv420p, 1920x800, 23.98 fps, 23.98 tbr, 1k tbn, 47.95 tbc (default)
// ffmpeg -i input.mkv -map 0:3 -map 0:2 -map 0:1 -map 0:0 -c copy output.mkv

// 使用滤镜（filtergraph）做为map参数来控制输出：
// ffmpeg -i INPUT -filter_complex "[0:0] scale=100x100[smaller_sized]"  -map "[smaller_sized]" out.mp4

// 一些流选择快捷方式，比如你也可以使用“0:v”：
// ffmpeg -i input -map 0:v -map 0:a output.mkv # chooses video and audio from input 0

// 重新编码视频流，但复制所有其他流（如音频、字幕、附件等）
// ffmpeg -i input.mkv -map 0 -c copy -c:v mpeg2video output.mkv

// 从同一个输入文件中仅提取音频流
// ffmpeg -i input.mkv -map 0:1 -map 0:2 -c copy output.mkv

// [0:v]，处理第一个输入的视频流
// ih[a]，输出标签为a
// [a][1:v]overlay 将第二个输入的视频流，覆盖在a标签上
// -filter_complex "[0:v]pad=iw*2:ih[a];[a][1:v]overlay=w"

// 创建多路文件输出，比如：
// ffmpeg -i input.mkv -map 0:1 -map 0:2 audios_only.mkv -map 0:0 video_only.mkv


// -itsoffset time_off设置时间偏移(s)，该选项影响所有后面的输入文件（只能用在输入文件前，偏移的那段时间背景为黑色）。该偏移被加到输入文件的时戳，定义一个正偏移意味着相应的流被延迟了 offset秒。 [-]hh:mm:ss[.xxx]的格式也支持



let option=
{

    "@copyright":'copyright',
    //设置版权

    "@threads":'threads',
    //设置线程数
    "@offset":"itsoffset",
    //设置时间偏移(s)，该选项影响所有后面的输入文件（只能用在输入文件前，偏移的那段时间背景为黑色）
    //该偏移被加到输入文件的时戳，定义一个正偏移意味着相应的流被延迟了 offset秒
    "@start":'ss',
    //00:00:03表示从第 00 分钟 03 秒开始，如果不加该命令，则从 0 秒开始制作；当用作输入选项时（在-i之前），在这个输入文件中寻找位置
    //请注意，在大多数格式中，不可能精确搜索，因此ffmpeg将在位置之前寻找最近的搜索点。 当转码和-accurate_seek被启用时（默认）
    //搜索点和位置之间的这个额外的分段将被解码和丢弃。 当进行流式复制或使用-noaccurate_seek时，它将被保留
    //当用作输出选项（在输出url之前）时，解码但丢弃输入，直到时间戳到达位置
    "@startEof":'sseof',
    //00:00:03 表示从尾部第 00 分钟 03 秒开始
    "@to":'to',
    //选项类似于-t选项，不同的是-to指定结束时刻，-t指定持续时间
    "@duration":'t',
    //3 表示持续 3 秒；用做输入选项（在-i之前），是限制读取输入文件的的时长
    "resloveLR":'hflip',
    //左右反转
    "resloveTB":'vflip',
    //上下反转
    "@worldNo":'sn',
    //取消字幕
    "@worldAdd":'c:s',
    //添加字幕
    
    "@force":'y',
    //覆盖输出文件
    "@exist":'n',
    //表示若某个输出文件已经存在则退出
    "@input":'i',
    //表示输入视频，可多个,只加此参数，可以获得文件信息
    "@output":'',
    //输出文件
    "@loop":'loop',
    //1 循环读输入 0读完就不读了
    "@copy":'c',
    //会覆盖/冲突其他改变视音频质量的操作
    "@format":'f',
    //格式

    "@rate":'r',
    //-r “15” 表示帧率，网上下载的视频帧率通常为 24，设为 15 效果挺好了，帧率越高体积越大，
    //如果不加该命令，则保持帧率不变；没有-vf的fps精准
    //用在-i之前，会改变时长
    //用在-i之后,不会影响时长
    "@size":'s',
    //-s 640x360 是分辨率，视频分辨率可能是 1080p，允许自定义，分辨率越高体积越大，如果不加该命令，则保持分辨率不变；
    "@bit":'b',
    //设置比特率，缺省200kb/s
    "@videoNo":'vn',
    //禁止视频输出，（转 mp3 要加个参数 -write_xing 0，不然生成的音频文件会显示双倍的总时间）
    "@code":'vcodec',
    //-vcodec/-c:v copy 复制音频原始编解码，用此方式失败时，省略此参数，直接强转，ffmpeg会更具后缀判断
    //-vcodec codec 强制使用codec编解码
    //会覆盖/冲突其他改变视频质量的操作
    "@frames":'vframes',
    //设置输出文件中包含的总帧数
    "@qv":'q:v',
    //2视频质量
    "@videoChannels":'vc',
    //视频通道
    "@aspect":'aspect',
    //视频比例，-aspect 16:9 会拉申视频
    "@limit":'fs',
    //限制文件大小 -fs 1000k (如果输出，那么输出文件大小，在1m左右)
    "@delay":'itsoffset',
    //-itsoffset 5 延迟5秒播放,影响所有后面的输入文件
    //正偏移意味着相应的流被延迟了 offset秒


    "@qa":'q:a',
    //2音频质量
    "@audioNo":'an',
    //禁止音频输出
    "@audioRate":'ar',
    //设置音频采样率
    "@audioBit":'ab',
    //设置音频码率(一般选择32、64、96、128)
    "@audioChannels":'ac',
    //设置通道 缺省为1
    "@volume":'vol',
    //200（200%的音量）
    "@audioCode":'acodec',
    //-acodec/-c:a copy 复制音频原始编解码，用此方式失败时，省略此参数，直接强转，ffmpeg会更具后缀判断
    //-acodec codec 强制使用codec编解码
    //会覆盖/冲突其他改变音频质量的操作


    "@filterComplex":'filter_complex',
    //多视频过滤
    //加速/减速视频
    //ffmpeg -i input.mp4 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2[a]" -map "[v]" -map "[a]" output.mp4
    //setpts=2*PTS 中的 2 是视频速度放慢一半  atempo=2 中的 2 是相对应的音频速度（=1/视频速度）
    //当快放时，当播放完所有帧，后面会一直显示最后一张图

    //setpts=2*PTS 中的 2 是视频速度放慢一半
    //atempo=2 中的 2 是相对应的音频速度（=1/视频速度）

    //colorchannelmixer  可以设置rgba四个分量的变换关系，共接受16个参数。 
    //rr 
    //rg 
    //rb 
    //ra 

    //gr 
    //gg 
    //gb 
    //ga 

    //br 
    //bg 
    //bb 
    //ba 

    //ar 
    //ag 
    //ab 
    //Aa 

    //参数本质就是加权求值时候的权重，每个参数接受0-1得值，滤镜本质是将每个颜色值加权求值.公式： 

    //red=red*rr+blue*rb+green*rg+alpha*ra 。green/blue和alpha同理。
    //参数默认值对应得是1，其他为0.如：rr，rg，rb，ra。其中rr默认为1，其他为0。 


    "@videoFilter":'vf',
    //视频过滤

    //-vf scale=1000:-1 视频宽度变为1000，比例不变。iw/ih 表示宽度/高度 -1 表示按输入尺寸等比自动计算
    //-vf crop=w=100:h=100:x=in_w/2:y=in_h 剪裁视屏
    //-vf "movie=xx.png[watermark];[in][watermark] overlay=10:10,1[out]" 10:10左上角坐标,1代表支持透明，xx.png水印图片
    //-i in.mp4 -vf crop="in_w/2:in_h/2:(in_w-out_w)/2+((in_w-out_w)/2)*sin(n/10):(in_h-out_h)/2+((in_h-out_h)/2)*sin(n/7)" out.mp4  

    //fps 帧率,没秒的帧数
    //tile=10x3 10*3的网格,常用把视频中的帧，剪切到一张图中
    //drawtext=fontfile=CourierNew.ttf:text='hello world':x=100:y=50:fontsize=24 写字
    //fade=in:st=0:d=5 从0开始，持续时间5秒，淡入淡出（in/out）
    //fade=in:0:5 从0开始，持续时间5帧，淡入淡出（in/out）
    //boxblur=2 模糊

    //colorbalance=rs=1
    //选项有三组，分别为rs/gs/bs、rm/gm/bm、rh/gh/bh。每一个选项都指定了目标颜色（rgb中的一个），s表现偏暗，m表现偏中间，h表现偏亮（且平滑）。
    //选项的值范围为[-1, 1]，正数表示偏向目标颜色，负数表示远离目标颜色。

    //colorchannelmixer 对rgba四个通道进行重新计算，并分别给定权重比例。
    //ffmpeg -i 3.jpg -vf colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3 colorchannelmixer3.jpg 
    //每个通道的权重从0到1，先给red的计算权重，即red=red*rr + blue*rb + green*rg + alpha*ra，然后分别是green跟blue的计算权重。

    //hue 改变色调，相当在调色板上调色
    //./ffmpeg -i 2.jpg -vf hue=h=30:s=1 hue_30.jpg
    //s为饱和度，范围为[-10,10]，默认为1。h为角度，不同角度有不同的颜色布局（类比调色板），范围为[0,360]，超过360则相当于从0开始，默认为0。

    //delogo 去logo
    //ffmpeg -i 2.jpg -vf delogo=x=0:y=0:w=50:h=50:band=5:show=1 delogo2.jpg 
    //band为模糊的强度，show为1时显示模糊的区域框，省略show或值为0时不显示区域框。

    //drawbox 加边框
    //ffmpeg -i 2.jpg -vf drawbox drawbox1.jpg 在边界画一个黑框
    //ffmpeg -i 2.jpg -vf drawbox=x=10:y=10:w=200:h=100:color=red@0.5:thickness=100 drawbox2.jpg     
    //0.5为边框的透明度（0到1） thickness为边框大小

    //drawbox 加网格
    //drawgrid=width=100:height=100:thickness=2:color=black@0.9 格子的宽高width，heigth

    //select
    //select=not(mod(n\\,30))选择视频中的那些帧

    //hflip 横向的倒置像素。另外还有vflip，是纵向的倒置。
    //deshake 删除相机抖动带来的负面影响

    //pad=width[:height:[:x[:y:[:color]]]]在视频帧上增加一快额外额区域，经常用在播放的时候显示不同的横纵比
    //pad=width=640:height=480:x=0:y=40:color=red

    "@reverse":'',//reverse视频倒放


    "@audioFilter":'af',
    //-af afade=t=in:st=0:d=3,afade=t=out:st=13:d=3 音频前3秒淡入，最后3秒淡出
    "@areverse":'',
    //areverse音频倒放

    "@shortest":'shortest',
    //当最短的输入流结束后即停止编码和输出
    //比如要合并video.avi 和 audio.mp3，音频audio.mp3时长比视频video的短
    "@deshake":'deshake',   
    //deshake 删除相机抖动带来的负面影响

}
/*-------------------------------------------------------------------------------------------*/


// 视频处理操作
/*-------------------------------------------------------------------------------------------*/ 
class ffmpeg
{   
    static com(obj,descript,times,rates,totals,widths,heights)
    {
        if(!obj.cwd)obj.cwd=$Config.root+'/extend/bin'
        if(!obj.query)throw Error('没有请求参数')

        let so=process.getegid?'':'.exe'

        let str=''
        for (const key in obj.query) 
        {
            if(key=='@videoFilter' || key=='@filterComplex')
            {
                obj.query[key]=`"${obj.query[key]}"`
            }

            let _key=ffmpeg.config(key)
            if(_key!=='')str+=` -${_key} `
            else str+=' '
            
            
            if(obj.query[key]!=='')str+=obj.query[key]
        }
        str=`ffmpeg${so} `+str;
        if(times)str=str.replace(/\$times/,times)
        if(rates)str=str.replace(/\$rates/,rates)
        if(totals)str=str.replace(/\$totals/,totals)
        if(widths)str=str.replace(/\$widths/,widths)
        if(heights)str=str.replace(/\$heights/,heights)
        $Sys.info(`[ffmpeg-${descript?descript:'请求'}]\r\n`,str);
        return $Sys.cmd(str,obj.cwd,obj.timeout)
    }

    static config(key)
    {
        if(key.indexOf('@')!==0)return key
        return (option[key]!==undefined)?option[key]:key.slice(1)
    }

    
    static proxy(target)
    {
        let handler=
        {
            get: function(target, name)
            {
                if(target[name])
                {
                    return target[name]
                }
                return (value)=>
                {
                    target.setKV(name,value)
                    return back
                }
            }
        }
        var back = new Proxy(target,handler)
        return back
    }

    
    static init(descript,timeout,cwd)
    {
        let target=new ffmpeg(cwd,timeout)
        target.descript=descript
        return ffmpeg.proxy(target)
    }

    constructor(cwd,timeout)
    {
        this.obj={timeout,cwd,query:{}}
        this.query=this.obj.query
    }

    descript(descri='')
    {
        this.descript=descri;
        return this
    }

    //需要知道视频尺寸
    //主流的采样方式有三种，YUV444p，YUV422p，YUV420p，数字越大质量越高
    //ffmpeg直接转成yuv，默认YUV420p
    yuvToVideo({size='640*480',fmt='yuv420p',format='mpeg4',input})
    {
        this.setKV('size',size)
        this.setKV('pix_fmt',fmt)
        if(input)this.setKV('input',input)
        this.setKV('code',format)
        return this
    }

    videoToYuv(fmt='yuv420p',input)
    {
        if(input)this.setKV('input',input)
        this.setKV('pix_fmt',fmt)
        return this
    }

    //输出.m3u8文件
    videoToM3u8({step=10,input})
    {
        if(input)this.setKV('input',input)
        this.setKV('c','copy')
        if(step)this.setKV('hls_time',step)
        return this
    }

    speed(val=2,input)
    {
        val=1/val
        if(input)this.setKV('input',input)
        this.setKV('filterComplex',`[0:v]setpts=${val}*PTS[v];[0:a]atempo=${val}[a]`)
        this.setKV('map',`[v]`)
        this.setKV('map',`[a]`)

        if(!isNaN(this.times) && val<1)
        {
            let time=parseInt(this.times*val)
            this.setKV('to',time)
        }
        return this
    }

    scale({val=0.5,width,height,auto,input})
    {
        if(input)this.setKV('input',input)
        if(auto && !isNaN(this.widths))
        {
            if(this.widths>this.heights)
            {
                let rate=auto/this.heights
                this.setKV('videoFilter',`scale=trunc(ih*${rate}/2)*2:${auto}`)
            }
            else
            {
                let rate=auto/this.widths
                this.setKV('videoFilter',`scale=${auto}:trunc(ih*${rate}/2)*2`)
            }
        }
        else if(width && !isNaN(this.widths))
        {
            let rate=width/this.widths
            this.setKV('videoFilter',`scale=${width}:trunc(ih*${rate}/2)*2`)
        }
        else if(height && !isNaN(this.heights))
        {
            let rate=height/this.heights
            this.setKV('videoFilter',`scale=trunc(ih*${rate}/2)*2:${height}`)
        }
        else if(val)this.setKV('videoFilter',`scale=trunc(iw*${val}/2)*2:trunc(ih*${val}/2)*2`)
        return this
    }

    corp({width=100,height=100,x='',y='',input})
    {
        if(!x)x=`(in_w/2)-${width/2}`
        if(!y)y=`(in_h/2)-${height/2}`

        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`crop=w=${width}:h=${height}:x=${x}:y=${y}`)
        return this
    }

    fade({start=0,duration=3,type='in',input})
    {
        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`fade=${type}:st=${start}:d=${duration}`)
        return this
    }

    audioFade()
    {
        if(input)this.setKV('input',input)
        this.setKV('audioFilter',`afade=${type}:st=${start}:d=${duration}`)
        return this
    }

    addGrid({w,h,x,y,blob=2,color='white',alpha=0.8,start=0,end=999999,input})
    {
        if(input)this.setKV('input',input)
        w=w?w:`(iw/2)+(${blob}/2)`
        h=h?h:`(ih/2)+(${blob}/2)`
        x=x?x:`-${blob}`
        y=y?y:`-${blob}`
        let font=`drawgrid=x=${x}:y=${y}:width=${w}:height=${h}:thickness=${blob}:`
        let fontNext=`color=${color}@${alpha}:enable='between(t\,${start}\,${end}\)'`
        this.setKV('videoFilter',font+fontNext)
        return this
    }

    addOver({w=100,h=20,x,y,blob=100,color='white',alpha=0.8,start=0,end=999999,input})
    {
        if(input)this.setKV('input',input)
        x=x?x:`(iw/2)-(w/2)`
        y=y?y:`(ih/2)-(h/2)`
        let font=`drawbox=x=${x}:y=${y}:width=${w}:height=${h}:thickness=${blob}:`
        let fontNext=`color=${color}@${alpha}:enable='between(t\,${start}\,${end}\)'`
        this.setKV('videoFilter',font+fontNext)
        return this
    }

    addText({text,size=40,alpha=0.8,color='white',x,y,start=0,end=999999,input})
    {
        if(input)this.setKV('input',input)
        x=x?x:`'(w-tw)/2'`
        y=y?y:`'(h-th)/2'`
        let font=`drawtext=fontcolor=${color}:alpha=${alpha}:fontsize=${size}:fontfile=font.ttf:`
        let fontNext=`text='${text}':x=${x}:y=${y}:enable='between(t\,${start}\,${end}\)'`
        this.setKV('videoFilter',font+fontNext)

        return this
    }

    addImage(img,x,y,input=null)
    {
        x=x?x:'(main_w-overlay_w)-(main_w-overlay_w)/2'
        y=y?y:'(main_h-overlay_h)-(main_h-overlay_h)/2'
        //main_w输入宽，overlay_w水印宽
        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`movie='${img.replace(':','\\:')}' [logo]; [in][logo] overlay=${x}:${y} [out]`)

        return this
    }

    reverse(input)
    {
        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`reverse`)
        return this
    }
    
    videoTB(i1,i2,h=2)
    {
        this.setKV('input',i1)
        this.setKV('input',i2)

        this.setKV('filterComplex',`[0:v]pad=iw:ih*${h}[a],[a][1:v]overlay=0:h`)
        return this
    }

    videoToImage(count='2*2',rate,input)
    {
        //每过rate帧抓一次
        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`select=not(mod(n\\,${rate})),tile=${count}`)
        return this
    }

    videoToGif(rate,frames,input)
    {
        if(input)this.setKV('input',input)
        this.setKV('rate',rate)
        this.setKV('frames',frames)
        return this
    }

    // tmpdir/image%4d.jpg
    imageToVideo(input=null,time=10,rate=1)
    {
        this.setKV('loop',1)
        this.setKV('format','image2')
        if(input)
        {
            this.setKV('input',input)
            this.setKV('code','libx264')
            this.setKV('duration',time)
            this.setKV('rate',rate)
            this.setKV('pix_fmt','yuv420p')
        }
        return this
    }

    //黑白效果
    stripColor(input=null)
    {
        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`lutyuv="u=128:v=128"`)

        return this
    }

    //模糊效果
    blur(blur=2,input=null)
    {
        if(input)this.setKV('input',input)
        this.setKV('videoFilter',`boxblur=${blur}`)
        return this
    }

    //连接视频,格式最好相同
    concatVideo(input=[])
    {
        if(input)this.setKV('input',`"concat:${input.join('|')}"`)
        this.setKV('copy','copy')
        return this
    }

    async info(input)
    {
        try 
        {
            if(input)this.setKV('input',input)
            await this.exec() 
        } 
        catch (result) 
        {
            let reg=/Duration\: ([0-9\:]*)/
            let regFps=/([0-9\.]*?) fps/
            let regWh=/, ([0-9]{1,5}?)x([0-9]{1,5})/

            let back=result.match(reg)
            if(back && back[1])
            {
                let time=0
                let cur=back[1].trim()
                cur=cur.split(':')
                time+=parseInt(cur[0])*60*60
                time+=parseInt(cur[1])*60
                time+=parseInt(cur[2])
                this.times=time
                let fps=result.match(regFps)
                if(fps && fps[1])
                {
                    this.rates=Number(fps[1])
                    this.totals=parseInt(this.rates*time)
                }
                
                let wh=result.match(regWh)
                if(wh && wh[1])
                {
                    this.widths=Number(wh[1])
                    this.heights=Number(wh[2])
                }
            }
            else
            {
                console.error(result)
                throw Error('获取时长失败')
            }
        }
        let next=this
        return {next}
    }
    
    setKV(key,value)
    {
        if(value===null)return
        if(key=='videoFilter' && this.query['@videoFilter'])
        {
            value=`${this.query['@videoFilter']},${value}`
        }
        else if(key=='input' && this.query['@input'])
        {
            value=`${this.query['@input']} -i ${value}`
        }
        else if(key=='map' && this.query['@map'])
        {
            value=`${this.query['@map']} -map ${value}`
        }
        this.query['@'+key]=(value!==undefined)?value:''
        return this
    }
    

    exec()
    {
        return ffmpeg.com(this.obj,this.descript,this.times,this.rates,this.totals,this.widths,this.heights)
    }

}
/*-------------------------------------------------------------------------------------------*/    
module.exports=ffmpeg;