diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..54f589e --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY: test +test: + go test -v ./... -cover \ No newline at end of file diff --git a/ffmpeg.go b/ffmpeg.go index 893f3ab..9988acd 100644 --- a/ffmpeg.go +++ b/ffmpeg.go @@ -2,72 +2,63 @@ package ffmpeg import ( "context" - "errors" - "fmt" + "log" "os/exec" - "strings" + + "fxkt.tech/ffmpeg/filter" + "fxkt.tech/ffmpeg/input" + "fxkt.tech/ffmpeg/output" ) type FFmpeg struct { - cmd string - infiles []string - filters []*Filter - outputs []*Output - + cmd string + y bool // y is yes for cover output file. + inputs input.Inputs + filters filter.Filters + outputs output.Outputs Sentence string } -func NewFFmpeg() *FFmpeg { +func Default() *FFmpeg { return &FFmpeg{ cmd: "ffmpeg", + y: true, } } -func (ff *FFmpeg) ChangeCmd(cmd string) { - ff.cmd = cmd +func (ff *FFmpeg) Yes(y bool) { + ff.y = y } -func (ff *FFmpeg) AddInputs(inputs ...string) { - ff.infiles = append(ff.infiles, inputs...) +func (ff *FFmpeg) AddInput(inputs ...*input.Input) { + ff.inputs = append(ff.inputs, inputs...) } -func (ff *FFmpeg) AddFilter(filters ...*Filter) { +func (ff *FFmpeg) AddFilter(filters ...*filter.Filter) { ff.filters = append(ff.filters, filters...) } -func (ff *FFmpeg) OutputGraph(output ...*Output) { - ff.outputs = output +func (ff *FFmpeg) AddOutput(outputs ...*output.Output) { + ff.outputs = append(ff.outputs, outputs...) } -func (ff *FFmpeg) combination() []string { - var params []string - params = append(params, "-y") - for _, infile := range ff.infiles { - params = append(params, "-i", infile) +func (ff *FFmpeg) Params() (params []string) { + if ff.y { + params = append(params, "-y") } - - var filtersStr []string - for _, filter := range ff.filters { - filtersStr = append(filtersStr, filter.String()) - } - params = append(params, "-filter_complex", strings.Join(filtersStr, ";")) - for _, op := range ff.outputs { - params = append(params, op.params...) - } - return params + params = append(params, ff.inputs.Params()...) + params = append(params, ff.filters.String()) + params = append(params, ff.outputs.Params()...) + return } -func (ff *FFmpeg) Run() error { - params := ff.combination() - cmd := exec.CommandContext(context.Background(), ff.cmd, params...) - ff.Sentence = cmd.String() - fmt.Println(ff.Sentence) - outBytes, err := cmd.CombinedOutput() - fmt.Println(string(outBytes)) - - if err != nil || cmd.ProcessState.ExitCode() != 0 { - sls := strings.Split(string(outBytes), "\n") - err = errors.New(strings.ReplaceAll(strings.Join(sls[len(sls)-4:len(sls)-1], "|"), "\n", "|")) +func (ff *FFmpeg) Run(ctx context.Context) (err error) { + cc := exec.CommandContext(ctx, ff.cmd, ff.Params()...) + ff.Sentence = cc.String() + retbytes, err := cc.CombinedOutput() + if err != nil { + return } - return err + log.Println(string(retbytes)) + return } diff --git a/ffmpeg_test.go b/ffmpeg_test.go new file mode 100644 index 0000000..05ff503 --- /dev/null +++ b/ffmpeg_test.go @@ -0,0 +1,41 @@ +package ffmpeg + +import ( + "context" + "testing" + + "fxkt.tech/ffmpeg/filter" + "fxkt.tech/ffmpeg/input" + "fxkt.tech/ffmpeg/output" +) + +func TestFFmpeg(t *testing.T) { + ff := Default() + ff.AddInput( + input.New( + input.SetI("vieo.mp4"), + ), + ) + ff.AddFilter( + filter.New( + filter.SetInStream("0"), + filter.SetContent("scale=trunc(oh*a/2)*2:720"), + filter.SetOutStream("x720"), + ), + filter.New( + filter.SetInStream("x720"), + filter.SetContent("delogo=0:0:100:100"), + filter.SetOutStream("xx720"), + ), + ) + ff.AddOutput( + output.New( + output.SetMap("xx720"), + output.SetMap("0:a"), + output.SetMetadata("comment=yilan888"), + output.SetFile("out720.mp4"), + ), + ) + ff.Run(context.Background()) + t.Log(ff.Sentence) +} diff --git a/filter.go b/filter.go deleted file mode 100644 index 9220254..0000000 --- a/filter.go +++ /dev/null @@ -1,30 +0,0 @@ -package ffmpeg - -import "fmt" - -type Filter struct { - alias string - content string - others []string -} - -func NewFilter(alias string, content string, others ...string) *Filter { - return &Filter{ - alias: alias, - content: content, - others: others, - } -} - -// -func (f *Filter) String() string { - var steamsAlias string - for _, other := range f.others { - steamsAlias = fmt.Sprintf("%s[%s]", steamsAlias, other) - } - var alias string - if f.alias != "" { - alias = fmt.Sprintf("[%s]", f.alias) - } - return fmt.Sprintf("%s%s%s", steamsAlias, f.content, alias) -} diff --git a/filter/filter.go b/filter/filter.go new file mode 100644 index 0000000..8e7b45f --- /dev/null +++ b/filter/filter.go @@ -0,0 +1,44 @@ +package filter + +import ( + "fmt" + "strings" +) + +type Filter struct { + // instreams []string + // content string + // outstreams []string + opt *option +} + +func New(opts ...OptionFunc) *Filter { + o := &option{} + for _, opt := range opts { + opt(o) + } + return &Filter{ + opt: o, + } +} + +func (f *Filter) Params() (params []string) { + if len(f.opt.instreams) != 0 { + for _, stream := range f.opt.instreams { + params = append(params, fmt.Sprintf("[%s]", stream)) + } + } + if f.opt.content != "" { + params = append(params, f.opt.content) + } + if len(f.opt.outstreams) != 0 { + for _, stream := range f.opt.outstreams { + params = append(params, fmt.Sprintf("[%s]", stream)) + } + } + return +} + +func (f *Filter) String() string { + return strings.Join(f.Params(), "") +} diff --git a/filter/filter_option.go b/filter/filter_option.go new file mode 100644 index 0000000..3d3153c --- /dev/null +++ b/filter/filter_option.go @@ -0,0 +1,27 @@ +package filter + +type OptionFunc func(*option) + +type option struct { + instreams []string + content string + outstreams []string +} + +func SetInStream(s string) OptionFunc { + return func(o *option) { + o.instreams = append(o.instreams, s) + } +} + +func SetContent(c string) OptionFunc { + return func(o *option) { + o.content = c + } +} + +func SetOutStream(s string) OptionFunc { + return func(o *option) { + o.outstreams = append(o.outstreams, s) + } +} diff --git a/filter/filters.go b/filter/filters.go new file mode 100644 index 0000000..9f013ca --- /dev/null +++ b/filter/filters.go @@ -0,0 +1,16 @@ +package filter + +import "strings" + +type Filters []*Filter + +func (filters Filters) Params() (params []string) { + for _, filter := range filters { + params = append(params, filter.String()) + } + return +} + +func (filters Filters) String() string { + return strings.Join(filters.Params(), ";") +} diff --git a/input/input.go b/input/input.go index d262f5f..747a4c9 100644 --- a/input/input.go +++ b/input/input.go @@ -5,22 +5,33 @@ import ( "strings" ) -// Input is 输入. +// Input is common input info. type Input struct { - i string // 输入的媒体文件 - ss int64 // 媒体文件选择的起始时间点 - t int64 //从起始时间开始的持续时间 - ext []string // 额外字段 + // i string // i is input file. + // ss int64 // ss is starttime. + // t int64 // t is duration. + // ext []string // extra params. + opt *option +} + +func New(opts ...OptionFunc) *Input { + o := &option{} + for _, opt := range opts { + opt(o) + } + return &Input{ + opt: o, + } } func (i *Input) Params() (params []string) { - if i.ss != 0 { - params = append(params, "-ss", strconv.FormatInt(i.ss, 10)) + if i.opt.ss != 0 { + params = append(params, "-ss", strconv.FormatInt(i.opt.ss, 10)) } - if i.t != 0 { - params = append(params, "-t", strconv.FormatInt(i.t, 10)) + if i.opt.t != 0 { + params = append(params, "-t", strconv.FormatInt(i.opt.t, 10)) } - params = append(params, "-i", i.i) + params = append(params, "-i", i.opt.i) return } diff --git a/input/input_option.go b/input/input_option.go new file mode 100644 index 0000000..4c6b930 --- /dev/null +++ b/input/input_option.go @@ -0,0 +1,16 @@ +package input + +type OptionFunc func(*option) + +type option struct { + i string // i is input file. + ss int64 // ss is starttime. + t int64 // t is duration. + ext []string // extra params. +} + +func SetI(i string) OptionFunc { + return func(o *option) { + o.i = i + } +} diff --git a/input/inputs.go b/input/inputs.go new file mode 100644 index 0000000..e558d17 --- /dev/null +++ b/input/inputs.go @@ -0,0 +1,10 @@ +package input + +type Inputs []*Input + +func (inputs Inputs) Params() (params []string) { + for _, input := range inputs { + params = append(params, input.Params()...) + } + return +} diff --git a/internal/pkg/strconv/strconv.go b/internal/pkg/strconv/strconv.go new file mode 100644 index 0000000..402b013 --- /dev/null +++ b/internal/pkg/strconv/strconv.go @@ -0,0 +1,31 @@ +package strconv + +import "strconv" + +func ToInt32(s string) (int32, error) { + i, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return 0, err + } + return int32(i), nil +} + +func ToInt64(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + +func ToInt32ByFloatString(s string) (int32, error) { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + return int32(f), nil +} + +func ToInt64ByFloatString(s string) (int64, error) { + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0, err + } + return int64(f), nil +} diff --git a/output/outout.go b/output/outout.go index 01d28b9..6a12877 100644 --- a/output/outout.go +++ b/output/outout.go @@ -1,6 +1,70 @@ package output +import ( + "fmt" + "strconv" + "strings" +) + +// Output is common output info. type Output struct { - maps []string - f string + // maps []string // mean is -map. + // cv, ca string // cv is c:v, ca is c:a. + // metadatas []string // mean is -metadata. + // threads int32 // thread counts, default 4. + // max_muxing_queue_size int32 // queue size when muxing, default 4086. + // movflags string // location of mp4 moov. + // f string // f is -f format. + // file string + opt *option +} + +func New(opts ...OptionFunc) *Output { + o := &option{ + threads: 4, + max_muxing_queue_size: 4086, + movflags: "faststart", + } + for _, opt := range opts { + opt(o) + } + return &Output{ + opt: o, + } +} + +func (o *Output) Params() (params []string) { + if len(o.opt.maps) != 0 { + for _, m := range o.opt.maps { + params = append(params, "-map", fmt.Sprintf("[%s]", m)) + } + } + if o.opt.cv != "" { + params = append(params, "c:v", o.opt.cv) + } + if o.opt.ca != "" { + params = append(params, "c:a", o.opt.ca) + } + if len(o.opt.metadatas) != 0 { + for _, m := range o.opt.metadatas { + params = append(params, "-metadata", m) + } + } + if o.opt.max_muxing_queue_size != 0 { + params = append(params, "-max_muxing_queue_size", strconv.FormatInt(int64(o.opt.max_muxing_queue_size), 10)) + } + if o.opt.movflags != "" { + params = append(params, "-movflags", o.opt.movflags) + } + if o.opt.f != "" { + params = append(params, "-f", o.opt.f) + } + if o.opt.file != "" { + params = append(params, o.opt.file) + } + return +} + +func (o *Output) String() string { + return strings.Join(o.Params(), " ") } diff --git a/output/output_option.go b/output/output_option.go new file mode 100644 index 0000000..3695a15 --- /dev/null +++ b/output/output_option.go @@ -0,0 +1,32 @@ +package output + +type OptionFunc func(*option) + +type option struct { + maps []string // mean is -map. + cv, ca string // cv is c:v, ca is c:a. + metadatas []string // mean is -metadata. + threads int32 // thread counts, default 4. + max_muxing_queue_size int32 // queue size when muxing, default 4086. + movflags string // location of mp4 moov. + f string // f is -f format. + file string +} + +func SetMap(m string) OptionFunc { + return func(o *option) { + o.maps = append(o.maps, m) + } +} + +func SetMetadata(md string) OptionFunc { + return func(o *option) { + o.metadatas = append(o.metadatas, md) + } +} + +func SetFile(f string) OptionFunc { + return func(o *option) { + o.file = f + } +} diff --git a/output/outputs.go b/output/outputs.go new file mode 100644 index 0000000..092ddab --- /dev/null +++ b/output/outputs.go @@ -0,0 +1,10 @@ +package output + +type Outputs []*Output + +func (Outputs Outputs) Params() (params []string) { + for _, output := range Outputs { + params = append(params, output.Params()...) + } + return +} diff --git a/question.md b/question.md index f4fbfa1..5a10b3c 100644 --- a/question.md +++ b/question.md @@ -27,7 +27,7 @@ ffmpeg -y -t 5 -i in.mp4 -i logo.png --filter_complex '[0]delogo=1490:40:400:100[p1];[p1]split=2[p1_hd][p1_sd];[p1_hd]scale=-1:720[p2_hd];[p2_hd][1]overlay=W-w-10:10[p_hd];[p1_sd]scale=-1:540[p2_sd];[p2_sd][1]overlay=W-w-10:10[p_sd]' +-filter_complex '[0]delogo=1490:40:400:100[p1];[p1]split=2[p1_hd][p1_sd];[p1_hd]scale=trunc(oh*a/2)*2:720[p2_hd];[p2_hd][1]overlay=W-w-10:10[p_hd];[p1_sd]scale=-1:540[p2_sd];[p2_sd][1]overlay=W-w-10:10[p_sd]' -map '[p_hd]' -map 0:a @@ -40,4 +40,8 @@ ffmpeg out_hd.mp4 -map '[p_sd]' -map 0:a -metadata comment=fu789sg -c:v libwz264 -c:a copy -threads 4 -max_muxing_queue_size 4086 -movflags faststart out_sd.mp4 -``` \ No newline at end of file + +trunc(oh*a/2)*2:720 +720:trunc(ow/a/2)*2 +``` + diff --git a/test/example_test.go b/test/example_test.go deleted file mode 100644 index 449b273..0000000 --- a/test/example_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package test - -import ( - "testing" - - "fxkt.tech/ffmpeg" -) - -func TestExample(t *testing.T) { - ff := ffmpeg.NewFFmpeg() - ff.AddInputs( - "video.mp4", - "logo.png", - ) - ff.AddFilter( - ffmpeg.NewFilter("dl", `delogo=0:0:400:200`, "0"), - ffmpeg.NewFilter("d_480][d_360", `split=2`, "dl"), - ffmpeg.NewFilter("rlt_480", `scale=trunc(oh*a/2)*2:480`, "d_480"), - ffmpeg.NewFilter("rlt_360", `scale=trunc(oh*a/2)*2:360`, "d_360"), - ) - ff.OutputGraph( - ffmpeg.NewOutput( - "-map", "[rlt_480]", - "-map", "0:a", - "-metadata", "comment=fu789sg", - "-c:v", "libx264", "-c:a", "copy", - "-threads", "4", - "-max_muxing_queue_size", "4086", - "-movflags", "faststart", - "out_480.mp4", - ), - ffmpeg.NewOutput( - "-map", "[rlt_360]", - "-map", "0:a", - "-metadata", "comment=fu789sg", - "-c:v", "libx264", "-c:a", "copy", - "-threads", "4", - "-max_muxing_queue_size", "4086", - "-movflags", "faststart", - "out_480.mp4", - ), - ) - err := ff.Run() - if err != nil { - t.Fatal(err) - } -}