├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── args.go ├── command.go ├── example ├── file │ └── file.go ├── pipe │ └── pipe.go ├── probe │ └── probe.go └── timeout │ └── timeout.go ├── fluentffmpeg_test └── args_test.go ├── getters_internal_test.go ├── go.mod ├── go.sum ├── input_getters.go ├── input_setters.go ├── output_getters.go ├── output_setters.go ├── probe.go └── util.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.15 20 | 21 | - name: Test 22 | run: go test -v ./... 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Modfy Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Fluent FFmpeg [![GoDoc](https://pkg.go.dev/badge/github.com/modfy/fluent-ffmpeg)](https://pkg.go.dev/github.com/modfy/fluent-ffmpeg) 2 | 3 | A Go version of [node-fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg). 4 | 5 | ## Installation 6 | `go get -u github.com/modfy/fluent-ffmpeg` 7 | 8 | ### Requirements 9 | You will need FFmpeg installed on your machine, or you can specify a path to a binary: 10 | 11 | ```go 12 | // Provide an empty string to use default FFmpeg path 13 | cmd := fluentffmpeg.NewCommand("") 14 | 15 | // Specify a path 16 | cmd = fluentffmpeg.NewCommand("/path/to/ffmpeg/binary") 17 | ``` 18 | 19 | ## Quick Start 20 | 21 | Create and run commands using an API similar to node-fluent-ffmpeg: 22 | 23 | ```go 24 | err := fluentffmpeg.NewCommand(""). 25 | InputPath("/path/to/video.avi"). 26 | OutputFormat("mp4"). 27 | OutputPath("/path/to/video.mp4"). 28 | Run() 29 | ``` 30 | 31 | You could use `context` to set the timeout: 32 | 33 | ```go 34 | ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5) 35 | defer cancel() 36 | err := fluentffmpeg.NewCommand(""). 37 | InputPath("/path/to/video.avi"). 38 | OutputFormat("mp4"). 39 | OutputPath("/path/to/video.mp4"). 40 | RunWithContext(ctx) 41 | ``` 42 | 43 | If you want to view the errors/logs returned from FFmpeg, provide an io.Writer to receive the data. 44 | ```go 45 | buf := &bytes.Buffer{} 46 | err := fluentffmpeg.NewCommand(""). 47 | InputPath("./video.avi"). 48 | OutputFormat("mp4"). 49 | OutputPath("./video.mp4"). 50 | Overwrite(true). 51 | OutputLogs(buf). // provide a io.Writer 52 | Run() 53 | 54 | out, _ := ioutil.ReadAll(buf) // read logs 55 | fmt.Println(string(out)) 56 | ``` 57 | 58 | You can also get the command in the form of an [exec.Cmd](https://golang.org/pkg/os/exec/#Cmd) struct, with which you can have better control over the running process. For example, you can conditionally kill the FFmpeg command: 59 | 60 | ```go 61 | done := make(chan error, 1) 62 | cmd := fluentffmpeg.NewCommand(""). 63 | InputPath("./video.avi"). 64 | OutputFormat("mp4"). 65 | OutputPath("./video.mp4"). 66 | Overwrite(true). 67 | Build() 68 | cmd.Start() 69 | 70 | go func() { 71 | done <- cmd.Wait() 72 | }() 73 | 74 | select { 75 | case <-time.After(time.Second * 5): 76 | fmt.Println("Timed out") 77 | cmd.Process.Kill() 78 | case <-done: 79 | } 80 | ``` 81 | 82 | FFprobe is also available for use and returns a map[string]interface{} of JSON data: 83 | ```go 84 | data := fluentffmpeg.Probe("./video.avi") 85 | ``` 86 | 87 | ## Credits 88 | 89 | This repo was inspired by [node-fluent-ffmpeg](https://github.com/fluent-ffmpeg/node-fluent-ffmpeg) and was built upon the work done by [@bitcodr](https://github.com/bitcodr/) in the https://github.com/bitcodr/gompeg 90 | 91 | ## Managed Version 92 | 93 | You can deploy this codebase yourself or you an entirely managed api from the creators at https://api.modfy.video -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | // Args contains the input and output args set for FFmpeg 4 | type Args struct { 5 | input inputArgs 6 | output outputArgs 7 | globalOptions []string 8 | } 9 | 10 | type inputArgs struct { 11 | inputPath string 12 | pipeInput bool 13 | fromFormat string 14 | nativeFramerateInput bool `getter:"none"` 15 | inputOptions []string 16 | } 17 | 18 | type outputArgs struct { 19 | outputPath string 20 | format string 21 | pipeOutput bool 22 | overwrite bool 23 | resolution string `getter:"none"` 24 | aspectRatio string 25 | pixelFormat string 26 | quality int 27 | preset string 28 | bufferSize int 29 | audioBitrate int 30 | audioChannels int 31 | keyframeInterval int 32 | audioCodec string 33 | constantRateFactor int 34 | videoBitRate int 35 | videoBitRateTolerance int 36 | videoMaxBitrate int 37 | videoMinBitrate int 38 | videoCodec string 39 | vFrames int 40 | frameRate int 41 | audioRate int 42 | outputOptions []string 43 | } 44 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os/exec" 7 | "reflect" 8 | "strings" 9 | 10 | "github.com/fatih/structs" 11 | ) 12 | 13 | // Command is a struct that holds arguments and their values to run FFmpeg 14 | type Command struct { 15 | FFmpegPath string 16 | Args *Args 17 | input io.Reader 18 | output io.Writer 19 | logs io.Writer 20 | } 21 | 22 | // NewCommand returns a new Command 23 | func NewCommand(ffmpegPath string) *Command { 24 | if ffmpegPath == "" { 25 | ffmpegPath = "ffmpeg" 26 | } 27 | return &Command{ 28 | FFmpegPath: ffmpegPath, 29 | Args: &Args{ 30 | output: outputArgs{ 31 | constantRateFactor: -1, // Initialize to -1 because zero value is a valid parameter 32 | }, 33 | }, 34 | } 35 | } 36 | 37 | // Run runs the FFmpeg command. It returns an error if the command fails with exit status code 1. This error message only signifies that 38 | // the command returned a non-zero status code, read from stderr to see more comprehensive FFmpeg errors. 39 | func (c *Command) Run() error { 40 | return c.Build().Run() 41 | } 42 | 43 | // RunWithContext is like Run but includes a context which is used to kill the process 44 | func (c *Command) RunWithContext(ctx context.Context) error { 45 | return c.BuildWithContext(ctx).Run() 46 | } 47 | 48 | // Build returns an exec.Cmd struct ready to run the FFmpeg command with its arguments 49 | func (c *Command) Build() *exec.Cmd { 50 | return c.BuildWithContext(context.Background()) 51 | } 52 | 53 | // BuildWithContext is like Build but includes a context which is used to kill the process 54 | func (c *Command) BuildWithContext(ctx context.Context) *exec.Cmd { 55 | cmd := exec.CommandContext(ctx, c.FFmpegPath, c.GetArgs()...) 56 | 57 | if c.input != nil { 58 | cmd.Stdin = c.input 59 | } 60 | 61 | if c.output != nil { 62 | cmd.Stdout = c.output 63 | } 64 | 65 | if c.logs != nil { 66 | cmd.Stderr = c.logs 67 | } 68 | 69 | return cmd 70 | } 71 | 72 | // GetArgs returns the arguments for the FFmpeg command. 73 | func (c *Command) GetArgs() []string { 74 | var options []string 75 | 76 | options = append(options, c.getArgs(c.Args.input, "pipeInput", "inputPath")...) 77 | options = append(options, c.getArgs(c.Args.output, "pipeOutput", "outputPath")...) 78 | 79 | return append(options, c.Args.globalOptions...) 80 | } 81 | 82 | func (c *Command) getArgs(argType interface{}, targetNames ...string) []string { 83 | var options []string 84 | var target []string 85 | 86 | fields := structs.Names(argType) 87 | 88 | // Iterates through the fields, 89 | // and calls its corresponding getter function. 90 | for _, v := range fields { 91 | option := true 92 | if containsString(targetNames, v) { 93 | option = false 94 | } 95 | value := reflect.ValueOf(c.Args).MethodByName("Get" + strings.Title(v)) 96 | if (value != reflect.Value{}) { 97 | result := value.Call([]reflect.Value{}) 98 | if v, ok := result[0].Interface().([]string); ok { 99 | if option { 100 | options = append(options, v...) 101 | } else { 102 | target = append(target, v...) 103 | } 104 | } 105 | } 106 | } 107 | return append(options, target...) 108 | } 109 | 110 | // OutputLogs sets the destination to write the FFmpeg log output to 111 | func (c *Command) OutputLogs(writer io.Writer) *Command { 112 | c.logs = writer 113 | return c 114 | } 115 | -------------------------------------------------------------------------------- /example/file/file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | 9 | fluentffmpeg "github.com/modfy/fluent-ffmpeg" 10 | ) 11 | 12 | func main() { 13 | buf := &bytes.Buffer{} 14 | err := fluentffmpeg.NewCommand(""). 15 | InputPath("./video.avi"). 16 | OutputFormat("mp4"). 17 | OutputPath("./video.mp4"). 18 | Overwrite(true). 19 | OutputLogs(buf). 20 | Run() 21 | 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | out, _ := ioutil.ReadAll(buf) 27 | fmt.Println(string(out)) 28 | } 29 | -------------------------------------------------------------------------------- /example/pipe/pipe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | fluentffmpeg "github.com/modfy/fluent-ffmpeg" 7 | ) 8 | 9 | func main() { 10 | http.HandleFunc("/ffmpeg", handle) 11 | 12 | http.ListenAndServe(":5000", nil) 13 | } 14 | 15 | func handle(w http.ResponseWriter, r *http.Request) { 16 | r.ParseMultipartForm(32 << 20) 17 | file, _, _ := r.FormFile("video") 18 | 19 | fluentffmpeg. 20 | NewCommand(""). 21 | PipeInput(file). 22 | OutputFormat("flv"). 23 | PipeOutput(w). 24 | Run() 25 | } 26 | -------------------------------------------------------------------------------- /example/probe/probe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | fluentffmpeg "github.com/modfy/fluent-ffmpeg" 7 | ) 8 | 9 | func main() { 10 | data, _ := fluentffmpeg.Probe("./video.avi") 11 | 12 | fmt.Println(data["format"]) 13 | } 14 | -------------------------------------------------------------------------------- /example/timeout/timeout.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | fluentffmpeg "github.com/modfy/fluent-ffmpeg" 8 | ) 9 | 10 | func main() { 11 | done := make(chan error, 1) 12 | cmd := fluentffmpeg.NewCommand(""). 13 | InputPath("./video.avi"). 14 | OutputFormat("mp4"). 15 | OutputPath("./video.mp4"). 16 | Overwrite(true). 17 | Build() 18 | cmd.Start() 19 | 20 | go func() { 21 | done <- cmd.Wait() 22 | }() 23 | 24 | select { 25 | case <-time.After(time.Second * 5): 26 | fmt.Println("Timed out") 27 | cmd.Process.Kill() 28 | case <-done: 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /fluentffmpeg_test/args_test.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg_test 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | fluentffmpeg "github.com/modfy/fluent-ffmpeg" 10 | ) 11 | 12 | func TestArgsOrder(t *testing.T) { 13 | buff := &bytes.Buffer{} 14 | 15 | desired := []string{"-f", "avi", "-test", "123", "-i", "pipe:0", "-f", "mp4", "-movflags", "empty_moov", "pipe:1", "-report"} 16 | args := fluentffmpeg.NewCommand(""). 17 | PipeInput(buff). 18 | InputOptions("-test", "123"). 19 | FromFormat("avi"). 20 | OutputFormat("mp4"). 21 | OutputOptions("-movflags", "empty_moov"). 22 | Options("-report"). 23 | PipeOutput(buff).GetArgs() 24 | 25 | if !reflect.DeepEqual(args, desired) { 26 | t.Errorf("Got wrong arguments. Expected: \"%s\", but got: \"%s\"", strings.Join(desired, " "), strings.Join(args, " ")) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /getters_internal_test.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/fatih/structs" 10 | ) 11 | 12 | func TestFieldsHaveGetters(t *testing.T) { 13 | cmd := NewCommand("") 14 | 15 | if err := testFieldsHaveGetters(cmd.Args.input, cmd); err != nil { 16 | t.Error(err.Error()) 17 | return 18 | } 19 | 20 | if err := testFieldsHaveGetters(cmd.Args.output, cmd); err != nil { 21 | t.Error(err.Error()) 22 | } 23 | } 24 | 25 | func testFieldsHaveGetters(argType interface{}, cmd *Command) error { 26 | 27 | for _, field := range structs.Names(argType) { 28 | sf, _ := reflect.TypeOf(argType).FieldByName(field) 29 | if sf.Tag.Get("getter") != "none" { 30 | method := reflect.ValueOf(cmd.Args).MethodByName("Get" + strings.Title(field)) 31 | if (method == reflect.Value{}) { 32 | return fmt.Errorf("field: (%s) has no getter", field) 33 | } 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/modfy/fluent-ffmpeg 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/fatih/structs v1.1.0 7 | github.com/pkg/errors v0.9.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 2 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 3 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | -------------------------------------------------------------------------------- /input_getters.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | // GetInputPath returns the input file path 4 | func (a *Args) GetInputPath() []string { 5 | if a.input.inputPath != "" { 6 | if a.input.nativeFramerateInput { 7 | return []string{"-re", "-i", a.input.inputPath} 8 | } 9 | return []string{"-i", a.input.inputPath} 10 | } 11 | return nil 12 | } 13 | 14 | // GetPipeInput returns whether or not ffmpeg is set to receive piped input 15 | func (a *Args) GetPipeInput() []string { 16 | if a.input.pipeInput != false { 17 | return []string{"-i", "pipe:0"} 18 | } 19 | 20 | return nil 21 | } 22 | 23 | // GetFromFormat returns the input format 24 | func (a *Args) GetFromFormat() []string { 25 | if a.input.fromFormat != "" { 26 | return []string{"-f", a.input.fromFormat} 27 | } 28 | 29 | return nil 30 | } 31 | 32 | // GetInputOptions returns additional input options 33 | func (a *Args) GetInputOptions() []string { 34 | return a.input.inputOptions 35 | } 36 | -------------------------------------------------------------------------------- /input_setters.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | import "io" 4 | 5 | // InputPath sets the path of the input file 6 | func (c *Command) InputPath(v string) *Command { 7 | c.Args.input.inputPath = v 8 | 9 | return c 10 | } 11 | 12 | // PipeInput sets the input to be read from an io.Reader 13 | func (c *Command) PipeInput(input io.Reader) *Command { 14 | c.Args.input.pipeInput = input != nil 15 | c.input = input 16 | 17 | return c 18 | } 19 | 20 | // FromFormat sets the format of the input 21 | func (c *Command) FromFormat(format string) *Command { 22 | c.Args.input.fromFormat = format 23 | return c 24 | } 25 | 26 | // InputOptions sets additional input options 27 | func (c *Command) InputOptions(options ...string) *Command { 28 | c.Args.input.inputOptions = options 29 | return c 30 | } 31 | 32 | // Options is intended for configuring global options that are not affected by their position in the FFmpeg command 33 | func (c *Command) Options(options ...string) *Command { 34 | c.Args.globalOptions = options 35 | 36 | return c 37 | } 38 | -------------------------------------------------------------------------------- /output_getters.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // GetAspectRatio returns the arguments for aspect ratio 10 | func (a *Args) GetAspectRatio() []string { 11 | if a.output.resolution != "" { 12 | resolution := strings.Split(a.output.resolution, "x") 13 | if len(resolution) != 0 { 14 | width, _ := strconv.ParseFloat(resolution[0], 64) 15 | height, _ := strconv.ParseFloat(resolution[1], 64) 16 | return []string{"-aspect", fmt.Sprintf("%f", width/height)} 17 | } 18 | } 19 | if a.output.aspectRatio != "" { 20 | return []string{"-aspect", a.output.aspectRatio} 21 | } 22 | return nil 23 | } 24 | 25 | // GetConstantRateFactor gets the constant rate factor (CRF) for video encoding 26 | func (a *Args) GetConstantRateFactor() []string { 27 | if a.output.constantRateFactor != -1 { 28 | return []string{"-crf", fmt.Sprintf("%d", a.output.constantRateFactor)} 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // GetVideoBitRate returns returns the arguments for video bit rate 35 | func (a *Args) GetVideoBitRate() []string { 36 | if a.output.videoBitRate != 0 { 37 | return []string{"-b:v", fmt.Sprintf("%d", a.output.videoBitRate)} 38 | } 39 | return nil 40 | } 41 | 42 | // GetVideoMaxBitrate returns the arguments for video max bit rate 43 | func (a *Args) GetVideoMaxBitrate() []string { 44 | if a.output.videoMaxBitrate != 0 { 45 | return []string{"-maxrate", fmt.Sprintf("%dk", a.output.videoMaxBitrate)} 46 | } 47 | return nil 48 | } 49 | 50 | // GetVideoMinBitrate returns the arguments for video min bit rate 51 | func (a *Args) GetVideoMinBitrate() []string { 52 | if a.output.videoMinBitrate != 0 { 53 | return []string{"-minrate", fmt.Sprintf("%dk", a.output.videoMinBitrate)} 54 | } 55 | return nil 56 | } 57 | 58 | // GetVideoBitRateTolerance returns the arguments for video bit rate tolerance 59 | func (a *Args) GetVideoBitRateTolerance() []string { 60 | if a.output.videoBitRateTolerance != 0 { 61 | return []string{"-bt", fmt.Sprintf("%dk", a.output.videoBitRateTolerance)} 62 | } 63 | return nil 64 | } 65 | 66 | // GetVideoCodec returns the arguments for video codec 67 | func (a *Args) GetVideoCodec() []string { 68 | if a.output.videoCodec != "" { 69 | return []string{"-c:v", a.output.videoCodec} 70 | } 71 | return nil 72 | } 73 | 74 | // GetVFrames returns the arguments for vframes 75 | func (a *Args) GetVFrames() []string { 76 | if a.output.vFrames != 0 { 77 | return []string{"-frames:v", fmt.Sprintf("%d", a.output.vFrames)} 78 | } 79 | return nil 80 | } 81 | 82 | // GetFrameRate returns the arguments for frame rate 83 | func (a *Args) GetFrameRate() []string { 84 | if a.output.frameRate != 0 { 85 | return []string{"-r", fmt.Sprintf("%d", a.output.frameRate)} 86 | } 87 | return nil 88 | } 89 | 90 | // GetAudioRate returns the arguments for audio rate 91 | func (a *Args) GetAudioRate() []string { 92 | if a.output.audioRate != 0 { 93 | return []string{"-ar", fmt.Sprintf("%d", a.output.audioRate)} 94 | } 95 | return nil 96 | } 97 | 98 | // GetAudioBitrate returns the arguments for bitrate 99 | func (a *Args) GetAudioBitrate() []string { 100 | if a.output.audioBitrate != 0 { 101 | return []string{"-b:a", fmt.Sprintf("%d", a.output.audioBitrate)} 102 | } 103 | return nil 104 | } 105 | 106 | // GetPreset returns the preset 107 | func (a *Args) GetPreset() []string { 108 | if a.output.preset != "" { 109 | return []string{"-preset", a.output.preset} 110 | } 111 | return nil 112 | } 113 | 114 | // GetBufferSize returns the buffer size 115 | func (a *Args) GetBufferSize() []string { 116 | if a.output.bufferSize != 0 { 117 | return []string{"-bufsize", fmt.Sprintf("%dk", a.output.bufferSize)} 118 | } 119 | return nil 120 | } 121 | 122 | // GetPixelFormat returns the pixel format 123 | func (a *Args) GetPixelFormat() []string { 124 | if a.output.pixelFormat != "" { 125 | return []string{"-pix_fmt", a.output.pixelFormat} 126 | } 127 | return nil 128 | } 129 | 130 | // GetKeyframeInterval returns the key frame interval 131 | func (a *Args) GetKeyframeInterval() []string { 132 | if a.output.keyframeInterval != 0 { 133 | return []string{"-g", fmt.Sprintf("%d", a.output.keyframeInterval)} 134 | } 135 | return nil 136 | } 137 | 138 | // GetAudioCodec returns the audio codec 139 | func (a *Args) GetAudioCodec() []string { 140 | if a.output.audioCodec != "" { 141 | return []string{"-c:a", a.output.audioCodec} 142 | } 143 | return nil 144 | } 145 | 146 | // GetAudioChannels returns the audio channels 147 | func (a *Args) GetAudioChannels() []string { 148 | if a.output.audioChannels != 0 { 149 | return []string{"-ac", fmt.Sprintf("%d", a.output.audioChannels)} 150 | } 151 | return nil 152 | } 153 | 154 | // GetFormat returns the output format 155 | func (a *Args) GetFormat() []string { 156 | if a.output.format != "" { 157 | return []string{"-f", a.output.format} 158 | } 159 | return nil 160 | } 161 | 162 | // GetQuality returns the quality 163 | func (a *Args) GetQuality() []string { 164 | if a.output.quality != 0 { 165 | return []string{"-crf", fmt.Sprintf("%d", a.output.quality)} 166 | } 167 | return nil 168 | } 169 | 170 | // GetOutputPath returns the output path 171 | func (a *Args) GetOutputPath() []string { 172 | if a.output.outputPath != "" { 173 | return []string{a.output.outputPath} 174 | } 175 | return nil 176 | } 177 | 178 | // GetPipeOutput returns whether or not ffmpeg is set to receive piped output 179 | func (a *Args) GetPipeOutput() []string { 180 | if a.output.pipeOutput != false { 181 | return []string{"pipe:1"} 182 | } 183 | 184 | return nil 185 | } 186 | 187 | // GetOverwrite returns whether or not FFmpeg is set to pipe its output 188 | func (a *Args) GetOverwrite() []string { 189 | if a.output.overwrite != false { 190 | return []string{"-y"} 191 | } 192 | 193 | return nil 194 | } 195 | 196 | // GetOutputOptions returns the additional output options 197 | func (a *Args) GetOutputOptions() []string { 198 | return a.output.outputOptions 199 | } 200 | -------------------------------------------------------------------------------- /output_setters.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | import "io" 4 | 5 | // AspectRatio gets the aspect ratio. 6 | // Ex: "16:9" 7 | func (c *Command) AspectRatio(v string) *Command { 8 | c.Args.output.aspectRatio = v 9 | 10 | return c 11 | } 12 | 13 | // Resolution gets the resolution of the media. Ex: "100x100" 14 | func (c *Command) Resolution(v string) *Command { 15 | c.Args.output.resolution = v 16 | 17 | return c 18 | } 19 | 20 | // ConstantRateFactor sets the constant rate factor (CRF) for video encoding 21 | func (c *Command) ConstantRateFactor(v int) *Command { 22 | c.Args.output.constantRateFactor = v 23 | 24 | return c 25 | } 26 | 27 | // VideoBitRate gets the video bit rate. 28 | func (c *Command) VideoBitRate(v int) *Command { 29 | c.Args.output.videoBitRate = v 30 | 31 | return c 32 | } 33 | 34 | // VideoMaxBitrate gets the max bit rate for the video. 35 | func (c *Command) VideoMaxBitrate(v int) *Command { 36 | c.Args.output.videoMaxBitrate = v 37 | 38 | return c 39 | } 40 | 41 | // VideoMinBitrate gets the max bit rate for the video. 42 | func (c *Command) VideoMinBitrate(v int) *Command { 43 | c.Args.output.videoMinBitrate = v 44 | 45 | return c 46 | } 47 | 48 | // VideoBitRateTolerance gets the video bit rate tolerance. 49 | func (c *Command) VideoBitRateTolerance(v int) *Command { 50 | c.Args.output.videoBitRateTolerance = v 51 | 52 | return c 53 | } 54 | 55 | // VideoCodec gets the desired video codec when working with video. 56 | func (c *Command) VideoCodec(v string) *Command { 57 | c.Args.output.videoCodec = v 58 | 59 | return c 60 | } 61 | 62 | // VFrames sets the number of frames to output 63 | func (c *Command) VFrames(v int) *Command { 64 | c.Args.output.vFrames = v 65 | 66 | return c 67 | } 68 | 69 | // FrameRate sets the frames per second 70 | func (c *Command) FrameRate(v int) *Command { 71 | c.Args.output.frameRate = v 72 | 73 | return c 74 | } 75 | 76 | // AudioRate sets the audio sampling rate 77 | func (c *Command) AudioRate(v int) *Command { 78 | c.Args.output.audioRate = v 79 | 80 | return c 81 | } 82 | 83 | // AudioBitRate sets the audio bit rate 84 | func (c *Command) AudioBitRate(v int) *Command { 85 | c.Args.output.audioBitrate = v 86 | 87 | return c 88 | } 89 | 90 | // NativeFramerateInput sets the native frame rate 91 | func (c *Command) NativeFramerateInput(v bool) *Command { 92 | c.Args.input.nativeFramerateInput = v 93 | 94 | return c 95 | } 96 | 97 | // Preset sets the preset 98 | func (c *Command) Preset(v string) *Command { 99 | c.Args.output.preset = v 100 | 101 | return c 102 | } 103 | 104 | // BufferSize sets the buffer size 105 | func (c *Command) BufferSize(v int) *Command { 106 | c.Args.output.bufferSize = v 107 | 108 | return c 109 | } 110 | 111 | // PixelFormat sets the pixel format. 112 | func (c *Command) PixelFormat(v string) *Command { 113 | c.Args.output.pixelFormat = v 114 | 115 | return c 116 | } 117 | 118 | // KeyframeInterval sets the keyframe interval. 119 | func (c *Command) KeyframeInterval(v int) *Command { 120 | c.Args.output.keyframeInterval = v 121 | 122 | return c 123 | } 124 | 125 | // AudioCodec sets the audio codec to use 126 | func (c *Command) AudioCodec(v string) *Command { 127 | c.Args.output.audioCodec = v 128 | 129 | return c 130 | } 131 | 132 | // AudioChannels sets the number of audio channels to use. 133 | func (c *Command) AudioChannels(v int) *Command { 134 | c.Args.output.audioChannels = v 135 | 136 | return c 137 | } 138 | 139 | // OutputFormat sets the format of the output 140 | func (c *Command) OutputFormat(v string) *Command { 141 | c.Args.output.format = v 142 | 143 | return c 144 | } 145 | 146 | // Quality sets the quality 147 | func (c *Command) Quality(v int) *Command { 148 | c.Args.output.quality = v 149 | 150 | return c 151 | } 152 | 153 | // OutputPath sets the path to write the output file 154 | func (c *Command) OutputPath(v string) *Command { 155 | c.Args.output.outputPath = v 156 | 157 | return c 158 | } 159 | 160 | // PipeOutput sets the output to be written to an io.Writer 161 | func (c *Command) PipeOutput(output io.Writer) *Command { 162 | c.Args.output.pipeOutput = output != nil 163 | c.output = output 164 | 165 | return c 166 | } 167 | 168 | // Overwrite configures FFmpeg to overwrite existing files 169 | func (c *Command) Overwrite(b bool) *Command { 170 | c.Args.output.overwrite = b 171 | 172 | return c 173 | } 174 | 175 | // OutputOptions sets additional output options 176 | func (c *Command) OutputOptions(options ...string) *Command { 177 | c.Args.output.outputOptions = options 178 | 179 | return c 180 | } 181 | -------------------------------------------------------------------------------- /probe.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os/exec" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ffprobePath = "ffprobe" 12 | 13 | // Probe runs ffprobe with the filePath as input and returns the response as JSON 14 | func Probe(filePath string) (map[string]interface{}, error) { 15 | cmd := exec.Command(ffprobePath, "-of", "json", "-show_streams", "-show_format", filePath) 16 | stdout, err := cmd.StdoutPipe() 17 | if err != nil { 18 | return nil, err 19 | } 20 | if err := cmd.Start(); err != nil { 21 | return nil, errors.Wrap(err, "Running ffprobe failed") 22 | } 23 | response := make(map[string]interface{}) 24 | err = json.NewDecoder(stdout).Decode(&response) 25 | if err != nil { 26 | errors.Wrap(err, "Failed to decode ffprobe data") 27 | return nil, err 28 | } 29 | if err := cmd.Wait(); err != nil { 30 | log.Fatal(err) 31 | return nil, errors.Wrap(err, "Running ffprobe failed") 32 | } 33 | 34 | return response, nil 35 | } 36 | 37 | // SetFfProbePath sets the path for the ffprobe executable 38 | func SetFfProbePath(path string) { 39 | ffprobePath = path 40 | } 41 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package fluentffmpeg 2 | 3 | func containsString(arr []string, str string) bool { 4 | for _, val := range arr { 5 | if val == str { 6 | return true 7 | } 8 | } 9 | 10 | return false 11 | } 12 | --------------------------------------------------------------------------------