├── docs ├── example_gif.gif ├── flowchart.png ├── flowchart2.png └── example_overlay.png ├── examples ├── sample_data │ ├── in1.mp4 │ ├── out1.jpeg │ ├── out2.jpeg │ ├── overlay.png │ ├── face-detect.jpg │ └── head-pose-face-detection-male-short.mp4 ├── readTimePositionAsJpeg.go ├── readFrameAsJpeg.go ├── limitcpu_test.go ├── showProgress.go ├── stream.go ├── example_test.go └── opencv_test.go ├── debugx.go ├── go.mod ├── probe_reader_test.go ├── graph.go ├── probe_test.go ├── ffmpeg_linux_test.go ├── debug.go ├── ffmpeg_windows_test.go ├── probe.go ├── probe_reader.go ├── view.go ├── run_linux.go ├── filters.go ├── utils.go ├── ffmpeg.go ├── go.sum ├── dag.go ├── README.md ├── node.go ├── run.go ├── LICENSE └── ffmpeg_test.go /docs/example_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/docs/example_gif.gif -------------------------------------------------------------------------------- /docs/flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/docs/flowchart.png -------------------------------------------------------------------------------- /docs/flowchart2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/docs/flowchart2.png -------------------------------------------------------------------------------- /docs/example_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/docs/example_overlay.png -------------------------------------------------------------------------------- /examples/sample_data/in1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/examples/sample_data/in1.mp4 -------------------------------------------------------------------------------- /examples/sample_data/out1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/examples/sample_data/out1.jpeg -------------------------------------------------------------------------------- /examples/sample_data/out2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/examples/sample_data/out2.jpeg -------------------------------------------------------------------------------- /examples/sample_data/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/examples/sample_data/overlay.png -------------------------------------------------------------------------------- /examples/sample_data/face-detect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/examples/sample_data/face-detect.jpg -------------------------------------------------------------------------------- /examples/sample_data/head-pose-face-detection-male-short.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u2takey/ffmpeg-go/HEAD/examples/sample_data/head-pose-face-detection-male-short.mp4 -------------------------------------------------------------------------------- /debugx.go: -------------------------------------------------------------------------------- 1 | // +build !debug 2 | 3 | package ffmpeg_go 4 | 5 | func DebugNodes(node []DagNode) {} 6 | 7 | func DebugOutGoingMap(node []DagNode, m map[int]map[Label][]NodeInfo) {} 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/u2takey/ffmpeg-go 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.38.20 7 | github.com/disintegration/imaging v1.6.2 8 | github.com/stretchr/testify v1.5.1 9 | github.com/u2takey/go-utils v0.3.1 10 | gocv.io/x/gocv v0.25.0 11 | ) 12 | -------------------------------------------------------------------------------- /probe_reader_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestProbeReader(t *testing.T) { 12 | f, err := os.Open(TestInputFile1) 13 | assert.Nil(t, err) 14 | 15 | data, err := ProbeReader(f, nil) 16 | assert.Nil(t, err) 17 | duration, err := probeOutputDuration(data) 18 | assert.Nil(t, err) 19 | assert.Equal(t, fmt.Sprintf("%f", duration), "7.036000") 20 | } -------------------------------------------------------------------------------- /examples/readTimePositionAsJpeg.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | 8 | ffmpeg "github.com/u2takey/ffmpeg-go" 9 | ) 10 | 11 | func ExampleReadTimePositionAsJpeg(inFileName string, seconds int) io.Reader { 12 | buf := bytes.NewBuffer(nil) 13 | err := ffmpeg.Input(inFileName, ffmpeg.KwArgs{"ss": seconds}). 14 | Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). 15 | WithOutput(buf, os.Stdout). 16 | Run() 17 | if err != nil { 18 | panic(err) 19 | } 20 | return buf 21 | } 22 | -------------------------------------------------------------------------------- /examples/readFrameAsJpeg.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | ffmpeg "github.com/u2takey/ffmpeg-go" 10 | ) 11 | 12 | func ExampleReadFrameAsJpeg(inFileName string, frameNum int) io.Reader { 13 | buf := bytes.NewBuffer(nil) 14 | err := ffmpeg.Input(inFileName). 15 | Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}). 16 | Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). 17 | WithOutput(buf, os.Stdout). 18 | Run() 19 | if err != nil { 20 | panic(err) 21 | } 22 | return buf 23 | } 24 | -------------------------------------------------------------------------------- /graph.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import "time" 4 | 5 | // for json spec 6 | 7 | type GraphNode struct { 8 | Name string `json:"name"` 9 | InputStreams []string `json:"input_streams"` 10 | OutputStreams []string `json:"output_streams"` 11 | Args Args `json:"args"` 12 | KwArgs KwArgs `json:"kw_args"` 13 | } 14 | 15 | type GraphOptions struct { 16 | Timeout time.Duration 17 | OverWriteOutput bool 18 | } 19 | 20 | type Graph struct { 21 | OutputStream string `json:"output_stream"` 22 | GraphOptions GraphOptions `json:"graph_options"` 23 | Nodes []GraphNode `json:"nodes"` 24 | } 25 | -------------------------------------------------------------------------------- /probe_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestProbe(t *testing.T) { 13 | data, err := Probe(TestInputFile1, nil) 14 | assert.Nil(t, err) 15 | duration, err := probeOutputDuration(data) 16 | assert.Nil(t, err) 17 | assert.Equal(t, fmt.Sprintf("%f", duration), "7.036000") 18 | } 19 | 20 | type probeFormat struct { 21 | Duration string `json:"duration"` 22 | } 23 | 24 | type probeData struct { 25 | Format probeFormat `json:"format"` 26 | } 27 | 28 | func probeOutputDuration(a string) (float64, error) { 29 | pd := probeData{} 30 | err := json.Unmarshal([]byte(a), &pd) 31 | if err != nil { 32 | return 0, err 33 | } 34 | f, err := strconv.ParseFloat(pd.Format.Duration, 64) 35 | if err != nil { 36 | return 0, err 37 | } 38 | return f, nil 39 | } 40 | -------------------------------------------------------------------------------- /ffmpeg_linux_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCompileWithOptions(t *testing.T) { 12 | out := Input("dummy.mp4").Output("dummy2.mp4") 13 | cmd := out.Compile(SeparateProcessGroup()) 14 | assert.Equal(t, 0, cmd.SysProcAttr.Pgid) 15 | assert.True(t, cmd.SysProcAttr.Setpgid) 16 | } 17 | 18 | func TestGlobalCommandOptions(t *testing.T) { 19 | out := Input("dummy.mp4").Output("dummy2.mp4") 20 | GlobalCommandOptions = append(GlobalCommandOptions, func(cmd *exec.Cmd) { 21 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} 22 | }) 23 | defer func() { 24 | GlobalCommandOptions = GlobalCommandOptions[0 : len(GlobalCommandOptions)-1] 25 | }() 26 | cmd := out.Compile() 27 | assert.Equal(t, 0, cmd.SysProcAttr.Pgid) 28 | assert.True(t, cmd.SysProcAttr.Setpgid) 29 | } 30 | -------------------------------------------------------------------------------- /debug.go: -------------------------------------------------------------------------------- 1 | // +build debug 2 | 3 | package ffmpeg_go 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "strings" 9 | ) 10 | 11 | func DebugNodes(node []DagNode) { 12 | b := strings.Builder{} 13 | for _, n := range node { 14 | b.WriteString(fmt.Sprintf("%s\n", n.String())) 15 | } 16 | log.Println(b.String()) 17 | } 18 | 19 | func DebugOutGoingMap(node []DagNode, m map[int]map[Label][]NodeInfo) { 20 | b := strings.Builder{} 21 | h := map[int]DagNode{} 22 | for _, n := range node { 23 | h[n.Hash()] = n 24 | } 25 | for k, v := range m { 26 | b.WriteString(fmt.Sprintf("[Key]: %s", h[k].String())) 27 | b.WriteString(" [Value]: {") 28 | for l, mm := range v { 29 | if l == "" { 30 | l = "None" 31 | } 32 | b.WriteString(fmt.Sprintf("%s: [", l)) 33 | for _, x := range mm { 34 | b.WriteString(x.Node.String()) 35 | b.WriteString(", ") 36 | } 37 | b.WriteString("]") 38 | } 39 | b.WriteString("}") 40 | b.WriteString("\n") 41 | } 42 | log.Println(b.String()) 43 | } 44 | -------------------------------------------------------------------------------- /ffmpeg_windows_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "os/exec" 5 | "syscall" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCompileWithOptions(t *testing.T) { 12 | out := Input("dummy.mp4").Output("dummy2.mp4") 13 | cmd := out.Compile(func(s *Stream, cmd *exec.Cmd) { 14 | if cmd.SysProcAttr == nil { 15 | cmd.SysProcAttr = &syscall.SysProcAttr{} 16 | } 17 | cmd.SysProcAttr.HideWindow = true 18 | }) 19 | assert.Equal(t, true, cmd.SysProcAttr.HideWindow) 20 | } 21 | 22 | func TestGlobalCommandOptions(t *testing.T) { 23 | out := Input("dummy.mp4").Output("dummy2.mp4") 24 | GlobalCommandOptions = append(GlobalCommandOptions, func(cmd *exec.Cmd) { 25 | if cmd.SysProcAttr == nil { 26 | cmd.SysProcAttr = &syscall.SysProcAttr{} 27 | } 28 | cmd.SysProcAttr.HideWindow = true 29 | }) 30 | defer func() { 31 | GlobalCommandOptions = GlobalCommandOptions[0 : len(GlobalCommandOptions)-1] 32 | }() 33 | cmd := out.Compile() 34 | assert.Equal(t, true, cmd.SysProcAttr.HideWindow) 35 | } 36 | -------------------------------------------------------------------------------- /examples/limitcpu_test.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package examples 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ffmpeg "github.com/u2takey/ffmpeg-go" 10 | ) 11 | 12 | func ComplexFilterExample(testInputFile, testOverlayFile, testOutputFile string) *ffmpeg.Stream { 13 | split := ffmpeg.Input(testInputFile).VFlip().Split() 14 | split0, split1 := split.Get("0"), split.Get("1") 15 | overlayFile := ffmpeg.Input(testOverlayFile).Crop(10, 10, 158, 112) 16 | return ffmpeg.Concat([]*ffmpeg.Stream{ 17 | split0.Trim(ffmpeg.KwArgs{"start_frame": 10, "end_frame": 20}), 18 | split1.Trim(ffmpeg.KwArgs{"start_frame": 30, "end_frame": 40})}). 19 | Overlay(overlayFile.HFlip(), ""). 20 | DrawBox(50, 50, 120, 120, "red", 5). 21 | Output(testOutputFile). 22 | OverWriteOutput() 23 | } 24 | 25 | // PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 26 | // 1386105 root 20 0 2114152 273780 31672 R 50.2 1.7 0:16.79 ffmpeg 27 | func TestLimitCpu(t *testing.T) { 28 | e := ComplexFilterExample("./sample_data/in1.mp4", "./sample_data/overlay.png", "./sample_data/out2.mp4") 29 | err := e.RunWithResource(0.1, 0.5) 30 | if err != nil { 31 | assert.Nil(t, err) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /probe.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "os/exec" 8 | "time" 9 | ) 10 | 11 | // Probe Run ffprobe on the specified file and return a JSON representation of the output. 12 | func Probe(fileName string, kwargs ...KwArgs) (string, error) { 13 | return ProbeWithTimeout(fileName, 0, MergeKwArgs(kwargs)) 14 | } 15 | 16 | func ProbeWithTimeout(fileName string, timeOut time.Duration, kwargs KwArgs) (string, error) { 17 | args := KwArgs{ 18 | "show_format": "", 19 | "show_streams": "", 20 | "of": "json", 21 | } 22 | 23 | return ProbeWithTimeoutExec(fileName, timeOut, MergeKwArgs([]KwArgs{args, kwargs})) 24 | } 25 | 26 | func ProbeWithTimeoutExec(fileName string, timeOut time.Duration, kwargs KwArgs) (string, error) { 27 | args := ConvertKwargsToCmdLineArgs(kwargs) 28 | args = append(args, fileName) 29 | ctx := context.Background() 30 | if timeOut > 0 { 31 | var cancel func() 32 | ctx, cancel = context.WithTimeout(context.Background(), timeOut) 33 | defer cancel() 34 | } 35 | cmd := exec.CommandContext(ctx, "ffprobe", args...) 36 | buf := bytes.NewBuffer(nil) 37 | stdErrBuf := bytes.NewBuffer(nil) 38 | cmd.Stdout = buf 39 | cmd.Stderr = stdErrBuf 40 | for _, option := range GlobalCommandOptions { 41 | option(cmd) 42 | } 43 | err := cmd.Run() 44 | if err != nil { 45 | return "", fmt.Errorf("[%s] %w", string(stdErrBuf.Bytes()), err) 46 | } 47 | return string(buf.Bytes()), nil 48 | } 49 | -------------------------------------------------------------------------------- /probe_reader.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os/exec" 9 | "time" 10 | ) 11 | 12 | // ProbeReader** functions are the same as Probe** but accepting io.Reader instead of fileName 13 | 14 | // ProbeReader runs ffprobe passing given reader via stdin and return a JSON representation of the output. 15 | func ProbeReader(r io.Reader, kwargs ...KwArgs) (string, error) { 16 | return ProbeReaderWithTimeout(r, 0, MergeKwArgs(kwargs)) 17 | } 18 | 19 | func ProbeReaderWithTimeout(r io.Reader, timeOut time.Duration, kwargs KwArgs) (string, error) { 20 | args := KwArgs{ 21 | "show_format": "", 22 | "show_streams": "", 23 | "of": "json", 24 | } 25 | 26 | return ProbeReaderWithTimeoutExec(r, timeOut, MergeKwArgs([]KwArgs{args, kwargs})) 27 | } 28 | 29 | func ProbeReaderWithTimeoutExec(r io.Reader, timeOut time.Duration, kwargs KwArgs) (string, error) { 30 | args := ConvertKwargsToCmdLineArgs(kwargs) 31 | args = append(args, "-") 32 | ctx := context.Background() 33 | if timeOut > 0 { 34 | var cancel func() 35 | ctx, cancel = context.WithTimeout(context.Background(), timeOut) 36 | defer cancel() 37 | } 38 | cmd := exec.CommandContext(ctx, "ffprobe", args...) 39 | cmd.Stdin = r 40 | buf := bytes.NewBuffer(nil) 41 | stdErrBuf := bytes.NewBuffer(nil) 42 | cmd.Stdout = buf 43 | cmd.Stderr = stdErrBuf 44 | err := cmd.Run() 45 | if err != nil { 46 | return "", fmt.Errorf("[%s] %w", string(stdErrBuf.Bytes()), err) 47 | } 48 | return string(buf.Bytes()), nil 49 | } 50 | -------------------------------------------------------------------------------- /examples/showProgress.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "math/rand" 8 | "net" 9 | "os" 10 | "path" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | ffmpeg "github.com/u2takey/ffmpeg-go" 17 | ) 18 | 19 | // ExampleShowProgress is an example of using the ffmpeg `-progress` option with a 20 | // unix-domain socket to report progress 21 | func ExampleShowProgress(inFileName, outFileName string) { 22 | a, err := ffmpeg.Probe(inFileName) 23 | if err != nil { 24 | panic(err) 25 | } 26 | totalDuration, err := probeDuration(a) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | err = ffmpeg.Input(inFileName). 32 | Output(outFileName, ffmpeg.KwArgs{"c:v": "libx264", "preset": "veryslow"}). 33 | GlobalArgs("-progress", "unix://"+TempSock(totalDuration)). 34 | OverWriteOutput(). 35 | Run() 36 | if err != nil { 37 | panic(err) 38 | } 39 | } 40 | 41 | func TempSock(totalDuration float64) string { 42 | // serve 43 | 44 | rand.Seed(time.Now().Unix()) 45 | sockFileName := path.Join(os.TempDir(), fmt.Sprintf("%d_sock", rand.Int())) 46 | l, err := net.Listen("unix", sockFileName) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | go func() { 52 | re := regexp.MustCompile(`out_time_ms=(\d+)`) 53 | fd, err := l.Accept() 54 | if err != nil { 55 | log.Fatal("accept error:", err) 56 | } 57 | buf := make([]byte, 16) 58 | data := "" 59 | progress := "" 60 | for { 61 | _, err := fd.Read(buf) 62 | if err != nil { 63 | return 64 | } 65 | data += string(buf) 66 | a := re.FindAllStringSubmatch(data, -1) 67 | cp := "" 68 | if len(a) > 0 && len(a[len(a)-1]) > 0 { 69 | c, _ := strconv.Atoi(a[len(a)-1][len(a[len(a)-1])-1]) 70 | cp = fmt.Sprintf("%.2f", float64(c)/totalDuration/1000000) 71 | } 72 | if strings.Contains(data, "progress=end") { 73 | cp = "done" 74 | } 75 | if cp == "" { 76 | cp = ".0" 77 | } 78 | if cp != progress { 79 | progress = cp 80 | fmt.Println("progress: ", progress) 81 | } 82 | } 83 | }() 84 | 85 | return sockFileName 86 | } 87 | 88 | type probeFormat struct { 89 | Duration string `json:"duration"` 90 | } 91 | 92 | type probeData struct { 93 | Format probeFormat `json:"format"` 94 | } 95 | 96 | func probeDuration(a string) (float64, error) { 97 | pd := probeData{} 98 | err := json.Unmarshal([]byte(a), &pd) 99 | if err != nil { 100 | return 0, err 101 | } 102 | f, err := strconv.ParseFloat(pd.Format.Duration, 64) 103 | if err != nil { 104 | return 0, err 105 | } 106 | return f, nil 107 | } 108 | -------------------------------------------------------------------------------- /view.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | type ViewType string 9 | 10 | const ( 11 | // FlowChart the diagram type for output in flowchart style (https://mermaid-js.github.io/mermaid/#/flowchart) (including current state 12 | ViewTypeFlowChart ViewType = "flowChart" 13 | // StateDiagram the diagram type for output in stateDiagram style (https://mermaid-js.github.io/mermaid/#/stateDiagram) 14 | ViewTypeStateDiagram ViewType = "stateDiagram" 15 | ) 16 | 17 | func (s *Stream) View(viewType ViewType) (string, error) { 18 | switch viewType { 19 | case ViewTypeFlowChart: 20 | return visualizeForMermaidAsFlowChart(s) 21 | case ViewTypeStateDiagram: 22 | return visualizeForMermaidAsStateDiagram(s) 23 | default: 24 | return "", fmt.Errorf("unknown ViewType: %s", viewType) 25 | } 26 | } 27 | 28 | func visualizeForMermaidAsStateDiagram(s *Stream) (string, error) { 29 | var buf bytes.Buffer 30 | 31 | nodes := getStreamSpecNodes([]*Stream{s}) 32 | var dagNodes []DagNode 33 | for i := range nodes { 34 | dagNodes = append(dagNodes, nodes[i]) 35 | } 36 | sorted, outGoingMap, err := TopSort(dagNodes) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | buf.WriteString("stateDiagram\n") 42 | 43 | for _, node := range sorted { 44 | next := outGoingMap[node.Hash()] 45 | for k, v := range next { 46 | for _, nextNode := range v { 47 | label := string(k) 48 | if label == "" { 49 | label = "<>" 50 | } 51 | buf.WriteString(fmt.Sprintf(` %s --> %s: %s`, node.ShortRepr(), nextNode.Node.ShortRepr(), label)) 52 | buf.WriteString("\n") 53 | } 54 | } 55 | } 56 | return buf.String(), nil 57 | } 58 | 59 | // visualizeForMermaidAsFlowChart outputs a visualization of a FSM in Mermaid format (including highlighting of current state). 60 | func visualizeForMermaidAsFlowChart(s *Stream) (string, error) { 61 | var buf bytes.Buffer 62 | 63 | nodes := getStreamSpecNodes([]*Stream{s}) 64 | var dagNodes []DagNode 65 | for i := range nodes { 66 | dagNodes = append(dagNodes, nodes[i]) 67 | } 68 | sorted, outGoingMap, err := TopSort(dagNodes) 69 | if err != nil { 70 | return "", err 71 | } 72 | buf.WriteString("graph LR\n") 73 | 74 | for _, node := range sorted { 75 | buf.WriteString(fmt.Sprintf(` %d[%s]`, node.Hash(), node.ShortRepr())) 76 | buf.WriteString("\n") 77 | } 78 | buf.WriteString("\n") 79 | 80 | for _, node := range sorted { 81 | next := outGoingMap[node.Hash()] 82 | for k, v := range next { 83 | for _, nextNode := range v { 84 | // todo ignore merged output 85 | label := string(k) 86 | if label == "" { 87 | label = "<>" 88 | } 89 | buf.WriteString(fmt.Sprintf(` %d --> |%s| %d`, node.Hash(), fmt.Sprintf("%s:%s", nextNode.Label, label), nextNode.Node.Hash())) 90 | buf.WriteString("\n") 91 | } 92 | } 93 | } 94 | 95 | buf.WriteString("\n") 96 | 97 | return buf.String(), nil 98 | } 99 | -------------------------------------------------------------------------------- /examples/stream.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | 9 | ffmpeg "github.com/u2takey/ffmpeg-go" 10 | ) 11 | 12 | // ExampleStream 13 | // inFileName: input filename 14 | // outFileName: output filename 15 | // dream: Use DeepDream frame processing (requires tensorflow) 16 | func ExampleStream(inFileName, outFileName string, dream bool) { 17 | if inFileName == "" { 18 | inFileName = "./in1.mp4" 19 | } 20 | if outFileName == "" { 21 | outFileName = "./out.mp4" 22 | } 23 | if dream { 24 | panic("Use DeepDream With Tensorflow haven't been implemented") 25 | } 26 | 27 | runExampleStream(inFileName, outFileName) 28 | } 29 | 30 | func getVideoSize(fileName string) (int, int) { 31 | log.Println("Getting video size for", fileName) 32 | data, err := ffmpeg.Probe(fileName) 33 | if err != nil { 34 | panic(err) 35 | } 36 | log.Println("got video info", data) 37 | type VideoInfo struct { 38 | Streams []struct { 39 | CodecType string `json:"codec_type"` 40 | Width int 41 | Height int 42 | } `json:"streams"` 43 | } 44 | vInfo := &VideoInfo{} 45 | err = json.Unmarshal([]byte(data), vInfo) 46 | if err != nil { 47 | panic(err) 48 | } 49 | for _, s := range vInfo.Streams { 50 | if s.CodecType == "video" { 51 | return s.Width, s.Height 52 | } 53 | } 54 | return 0, 0 55 | } 56 | 57 | func startFFmpegProcess1(infileName string, writer io.WriteCloser) <-chan error { 58 | log.Println("Starting ffmpeg process1") 59 | done := make(chan error) 60 | go func() { 61 | err := ffmpeg.Input(infileName). 62 | Output("pipe:", 63 | ffmpeg.KwArgs{ 64 | "format": "rawvideo", "pix_fmt": "rgb24", 65 | }). 66 | WithOutput(writer). 67 | Run() 68 | log.Println("ffmpeg process1 done") 69 | _ = writer.Close() 70 | done <- err 71 | close(done) 72 | }() 73 | return done 74 | } 75 | 76 | func startFFmpegProcess2(outfileName string, buf io.Reader, width, height int) <-chan error { 77 | log.Println("Starting ffmpeg process2") 78 | done := make(chan error) 79 | go func() { 80 | err := ffmpeg.Input("pipe:", 81 | ffmpeg.KwArgs{"format": "rawvideo", 82 | "pix_fmt": "rgb24", "s": fmt.Sprintf("%dx%d", width, height), 83 | }). 84 | Output(outfileName, ffmpeg.KwArgs{"pix_fmt": "yuv420p"}). 85 | OverWriteOutput(). 86 | WithInput(buf). 87 | Run() 88 | log.Println("ffmpeg process2 done") 89 | done <- err 90 | close(done) 91 | }() 92 | return done 93 | } 94 | 95 | func process(reader io.ReadCloser, writer io.WriteCloser, w, h int) { 96 | go func() { 97 | frameSize := w * h * 3 98 | buf := make([]byte, frameSize, frameSize) 99 | for { 100 | n, err := io.ReadFull(reader, buf) 101 | if n == 0 || err == io.EOF { 102 | _ = writer.Close() 103 | return 104 | } else if n != frameSize || err != nil { 105 | panic(fmt.Sprintf("read error: %d, %s", n, err)) 106 | } 107 | for i := range buf { 108 | buf[i] = buf[i] / 3 109 | } 110 | n, err = writer.Write(buf) 111 | if n != frameSize || err != nil { 112 | panic(fmt.Sprintf("write error: %d, %s", n, err)) 113 | } 114 | } 115 | }() 116 | return 117 | } 118 | 119 | func runExampleStream(inFile, outFile string) { 120 | w, h := getVideoSize(inFile) 121 | log.Println(w, h) 122 | 123 | pr1, pw1 := io.Pipe() 124 | pr2, pw2 := io.Pipe() 125 | done1 := startFFmpegProcess1(inFile, pw1) 126 | process(pr1, pw2, w, h) 127 | done2 := startFFmpegProcess2(outFile, pr2, w, h) 128 | err := <-done1 129 | if err != nil { 130 | panic(err) 131 | } 132 | err = <-done2 133 | if err != nil { 134 | panic(err) 135 | } 136 | log.Println("Done") 137 | } 138 | -------------------------------------------------------------------------------- /examples/example_test.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/credentials" 8 | "github.com/disintegration/imaging" 9 | "github.com/stretchr/testify/assert" 10 | ffmpeg "github.com/u2takey/ffmpeg-go" 11 | ) 12 | 13 | // 14 | // More simple examples please refer to ffmpeg_test.go 15 | // 16 | 17 | func TestExampleStream(t *testing.T) { 18 | ExampleStream("./sample_data/in1.mp4", "./sample_data/out1.mp4", false) 19 | } 20 | 21 | func TestExampleReadFrameAsJpeg(t *testing.T) { 22 | reader := ExampleReadFrameAsJpeg("./sample_data/in1.mp4", 5) 23 | img, err := imaging.Decode(reader) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | err = imaging.Save(img, "./sample_data/out1.jpeg") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func TestExampleReadTimePositionAsJpeg(t *testing.T) { 34 | reader := ExampleReadTimePositionAsJpeg("./sample_data/in1.mp4", 4) 35 | img, err := imaging.Decode(reader) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | err = imaging.Save(img, "./sample_data/out2.jpeg") 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | } 44 | 45 | func TestExampleShowProgress(t *testing.T) { 46 | ExampleShowProgress("./sample_data/in1.mp4", "./sample_data/out2.mp4") 47 | } 48 | 49 | func TestSimpleS3StreamExample(t *testing.T) { 50 | err := ffmpeg.Input("./sample_data/in1.mp4", nil). 51 | Output("s3://data-1251825869/test_out.ts", ffmpeg.KwArgs{ 52 | "aws_config": &aws.Config{ 53 | Credentials: credentials.NewStaticCredentials("xx", "yyy", ""), 54 | //Endpoint: aws.String("xx"), 55 | Region: aws.String("yyy"), 56 | }, 57 | // outputS3 use stream output, so you can only use supported format 58 | // if you want mp4 format for example, you can output it to a file, and then call s3 sdk to do upload 59 | "format": "mpegts", 60 | }). 61 | Run() 62 | assert.Nil(t, err) 63 | } 64 | 65 | func TestExampleChangeCodec(t *testing.T) { 66 | err := ffmpeg.Input("./sample_data/in1.mp4"). 67 | Output("./sample_data/out1.mp4", ffmpeg.KwArgs{"c:v": "libx265"}). 68 | OverWriteOutput().ErrorToStdOut().Run() 69 | assert.Nil(t, err) 70 | } 71 | 72 | func TestExampleCutVideo(t *testing.T) { 73 | err := ffmpeg.Input("./sample_data/in1.mp4", ffmpeg.KwArgs{"ss": 1}). 74 | Output("./sample_data/out1.mp4", ffmpeg.KwArgs{"t": 1}).OverWriteOutput().Run() 75 | assert.Nil(t, err) 76 | } 77 | 78 | func TestExampleScaleVideo(t *testing.T) { 79 | err := ffmpeg.Input("./sample_data/in1.mp4"). 80 | Output("./sample_data/out1.mp4", ffmpeg.KwArgs{"vf": "scale=w=480:h=240"}). 81 | OverWriteOutput().ErrorToStdOut().Run() 82 | assert.Nil(t, err) 83 | } 84 | 85 | func TestExampleAddWatermark(t *testing.T) { 86 | // show watermark with size 64:-1 in the top left corner after seconds 1 87 | overlay := ffmpeg.Input("./sample_data/overlay.png").Filter("scale", ffmpeg.Args{"64:-1"}) 88 | err := ffmpeg.Filter( 89 | []*ffmpeg.Stream{ 90 | ffmpeg.Input("./sample_data/in1.mp4"), 91 | overlay, 92 | }, "overlay", ffmpeg.Args{"10:10"}, ffmpeg.KwArgs{"enable": "gte(t,1)"}). 93 | Output("./sample_data/out1.mp4").OverWriteOutput().ErrorToStdOut().Run() 94 | assert.Nil(t, err) 95 | } 96 | 97 | func TestExampleCutVideoForGif(t *testing.T) { 98 | err := ffmpeg.Input("./sample_data/in1.mp4", ffmpeg.KwArgs{"ss": "1"}). 99 | Output("./sample_data/out1.gif", ffmpeg.KwArgs{"s": "320x240", "pix_fmt": "rgb24", "t": "3", "r": "3"}). 100 | OverWriteOutput().ErrorToStdOut().Run() 101 | assert.Nil(t, err) 102 | } 103 | 104 | func TestExampleMultipleOutput(t *testing.T) { 105 | input := ffmpeg.Input("./sample_data/in1.mp4").Split() 106 | out1 := input.Get("0").Filter("scale", ffmpeg.Args{"1920:-1"}). 107 | Output("./sample_data/1920.mp4", ffmpeg.KwArgs{"b:v": "5000k"}) 108 | out2 := input.Get("1").Filter("scale", ffmpeg.Args{"1280:-1"}). 109 | Output("./sample_data/1280.mp4", ffmpeg.KwArgs{"b:v": "2800k"}) 110 | 111 | err := ffmpeg.MergeOutputs(out1, out2).OverWriteOutput().ErrorToStdOut().Run() 112 | assert.Nil(t, err) 113 | } 114 | -------------------------------------------------------------------------------- /run_linux.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strconv" 11 | "syscall" 12 | 13 | "github.com/u2takey/go-utils/rand" 14 | ) 15 | 16 | const ( 17 | cgroupConfigKey = "cgroupConfig" 18 | cpuRoot = "/sys/fs/cgroup/cpu,cpuacct" 19 | cpuSetRoot = "/sys/fs/cgroup/cpuset" 20 | procsFile = "cgroup.procs" 21 | cpuSharesFile = "cpu.shares" 22 | cfsPeriodUsFile = "cpu.cfs_period_us" 23 | cfsQuotaUsFile = "cpu.cfs_quota_us" 24 | cpuSetCpusFile = "cpuset.cpus" 25 | cpuSetMemsFile = "cpuset.mems" 26 | ) 27 | 28 | type cgroupConfig struct { 29 | cpuRequest float32 30 | cpuLimit float32 31 | cpuset string 32 | memset string 33 | } 34 | 35 | func (s *Stream) setCGroupConfig(f func(config *cgroupConfig)) *Stream { 36 | a := s.Context.Value(cgroupConfigKey) 37 | if a == nil { 38 | a = &cgroupConfig{} 39 | } 40 | f(a.(*cgroupConfig)) 41 | s.Context = context.WithValue(s.Context, cgroupConfigKey, a) 42 | return s 43 | } 44 | 45 | func (s *Stream) WithCpuCoreRequest(n float32) *Stream { 46 | return s.setCGroupConfig(func(config *cgroupConfig) { 47 | config.cpuRequest = n 48 | }) 49 | } 50 | 51 | func (s *Stream) WithCpuCoreLimit(n float32) *Stream { 52 | return s.setCGroupConfig(func(config *cgroupConfig) { 53 | config.cpuLimit = n 54 | }) 55 | } 56 | 57 | func (s *Stream) WithCpuSet(n string) *Stream { 58 | return s.setCGroupConfig(func(config *cgroupConfig) { 59 | config.cpuset = n 60 | }) 61 | } 62 | 63 | func (s *Stream) WithMemSet(n string) *Stream { 64 | return s.setCGroupConfig(func(config *cgroupConfig) { 65 | config.memset = n 66 | }) 67 | } 68 | 69 | func writeCGroupFile(rootPath, file string, value string) error { 70 | return ioutil.WriteFile(filepath.Join(rootPath, file), []byte(value), 0755) 71 | } 72 | 73 | func (s *Stream) RunWithResource(cpuRequest, cpuLimit float32) error { 74 | return s.WithCpuCoreRequest(cpuRequest).WithCpuCoreLimit(cpuLimit).RunLinux() 75 | } 76 | 77 | func (s *Stream) RunLinux() error { 78 | a := s.Context.Value(cgroupConfigKey).(*cgroupConfig) 79 | if a.cpuRequest > a.cpuLimit { 80 | return errors.New("cpuCoreLimit should greater or equal to cpuCoreRequest") 81 | } 82 | name := "ffmpeg_go_" + rand.String(6) 83 | rootCpuPath, rootCpuSetPath := filepath.Join(cpuRoot, name), filepath.Join(cpuSetRoot, name) 84 | err := os.MkdirAll(rootCpuPath, 0777) 85 | if err != nil { 86 | return err 87 | } 88 | err = os.MkdirAll(rootCpuSetPath, 0777) 89 | if err != nil { 90 | return err 91 | } 92 | defer func() { _ = os.Remove(rootCpuPath); _ = os.Remove(rootCpuSetPath) }() 93 | 94 | share := int(1024 * a.cpuRequest) 95 | period := 100000 96 | quota := int(a.cpuLimit * 100000) 97 | 98 | if share > 0 { 99 | err = writeCGroupFile(rootCpuPath, cpuSharesFile, strconv.Itoa(share)) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | err = writeCGroupFile(rootCpuPath, cfsPeriodUsFile, strconv.Itoa(period)) 105 | if err != nil { 106 | return err 107 | } 108 | if quota > 0 { 109 | err = writeCGroupFile(rootCpuPath, cfsQuotaUsFile, strconv.Itoa(quota)) 110 | if err != nil { 111 | return err 112 | } 113 | } 114 | if a.cpuset != "" && a.memset != "" { 115 | err = writeCGroupFile(rootCpuSetPath, cpuSetCpusFile, a.cpuset) 116 | if err != nil { 117 | return err 118 | } 119 | err = writeCGroupFile(rootCpuSetPath, cpuSetMemsFile, a.memset) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | 125 | cmd := s.Compile() 126 | err = cmd.Start() 127 | if err != nil { 128 | return err 129 | } 130 | if share > 0 || quota > 0 { 131 | err = writeCGroupFile(rootCpuPath, procsFile, strconv.Itoa(cmd.Process.Pid)) 132 | if err != nil { 133 | return err 134 | } 135 | } 136 | if a.cpuset != "" && a.memset != "" { 137 | err = writeCGroupFile(rootCpuSetPath, procsFile, strconv.Itoa(cmd.Process.Pid)) 138 | if err != nil { 139 | return err 140 | } 141 | } 142 | 143 | return cmd.Wait() 144 | } 145 | 146 | // SeparateProcessGroup ensures that the command is run in a separate process 147 | // group. This is useful to enable handling of signals such as SIGINT without 148 | // propagating them to the ffmpeg process. 149 | func SeparateProcessGroup() CompilationOption { 150 | return func(s *Stream, cmd *exec.Cmd) { 151 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pgid: 0} 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /filters.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func AssertType(hasType, expectType string, action string) { 9 | if hasType != expectType { 10 | panic(fmt.Sprintf("cannot %s on non-%s", action, expectType)) 11 | } 12 | } 13 | 14 | func FilterMultiOutput(streamSpec []*Stream, filterName string, args Args, kwArgs ...KwArgs) *Node { 15 | return NewFilterNode(filterName, streamSpec, -1, args, MergeKwArgs(kwArgs)) 16 | } 17 | 18 | func Filter(streamSpec []*Stream, filterName string, args Args, kwArgs ...KwArgs) *Stream { 19 | return FilterMultiOutput(streamSpec, filterName, args, MergeKwArgs(kwArgs)).Stream("", "") 20 | } 21 | 22 | func (s *Stream) Filter(filterName string, args Args, kwArgs ...KwArgs) *Stream { 23 | AssertType(s.Type, "FilterableStream", "filter") 24 | return Filter([]*Stream{s}, filterName, args, MergeKwArgs(kwArgs)) 25 | } 26 | 27 | func (s *Stream) Split() *Node { 28 | AssertType(s.Type, "FilterableStream", "split") 29 | return NewFilterNode("split", []*Stream{s}, 1, nil, nil) 30 | } 31 | 32 | func (s *Stream) ASplit() *Node { 33 | AssertType(s.Type, "FilterableStream", "asplit") 34 | return NewFilterNode("asplit", []*Stream{s}, 1, nil, nil) 35 | } 36 | 37 | func (s *Stream) SetPts(expr string) *Node { 38 | AssertType(s.Type, "FilterableStream", "setpts") 39 | return NewFilterNode("setpts", []*Stream{s}, 1, []string{expr}, nil) 40 | } 41 | 42 | func (s *Stream) Trim(kwargs ...KwArgs) *Stream { 43 | AssertType(s.Type, "FilterableStream", "trim") 44 | return NewFilterNode("trim", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") 45 | } 46 | 47 | func (s *Stream) Overlay(overlayParentNode *Stream, eofAction string, kwargs ...KwArgs) *Stream { 48 | AssertType(s.Type, "FilterableStream", "overlay") 49 | if eofAction == "" { 50 | eofAction = "repeat" 51 | } 52 | args := MergeKwArgs(kwargs) 53 | args["eof_action"] = eofAction 54 | return NewFilterNode("overlay", []*Stream{s, overlayParentNode}, 2, nil, args).Stream("", "") 55 | } 56 | 57 | func (s *Stream) HFlip(kwargs ...KwArgs) *Stream { 58 | AssertType(s.Type, "FilterableStream", "hflip") 59 | return NewFilterNode("hflip", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") 60 | } 61 | 62 | func (s *Stream) VFlip(kwargs ...KwArgs) *Stream { 63 | AssertType(s.Type, "FilterableStream", "vflip") 64 | return NewFilterNode("vflip", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") 65 | } 66 | 67 | func (s *Stream) Crop(x, y, w, h int, kwargs ...KwArgs) *Stream { 68 | AssertType(s.Type, "FilterableStream", "crop") 69 | return NewFilterNode("crop", []*Stream{s}, 1, []string{ 70 | strconv.Itoa(w), strconv.Itoa(h), strconv.Itoa(x), strconv.Itoa(y), 71 | }, MergeKwArgs(kwargs)).Stream("", "") 72 | } 73 | 74 | func (s *Stream) DrawBox(x, y, w, h int, color string, thickness int, kwargs ...KwArgs) *Stream { 75 | AssertType(s.Type, "FilterableStream", "drawbox") 76 | args := MergeKwArgs(kwargs) 77 | if thickness != 0 { 78 | args["t"] = thickness 79 | } 80 | return NewFilterNode("drawbox", []*Stream{s}, 1, []string{ 81 | strconv.Itoa(x), strconv.Itoa(y), strconv.Itoa(w), strconv.Itoa(h), color, 82 | }, args).Stream("", "") 83 | } 84 | 85 | func (s *Stream) Drawtext(text string, x, y int, escape bool, kwargs ...KwArgs) *Stream { 86 | AssertType(s.Type, "FilterableStream", "drawtext") 87 | args := MergeKwArgs(kwargs) 88 | if escape && text != "" { 89 | text = fmt.Sprintf("%q", text) 90 | } 91 | if text != "" { 92 | args["text"] = text 93 | } 94 | if x != 0 { 95 | args["x"] = x 96 | } 97 | 98 | if y != 0 { 99 | args["y"] = y 100 | } 101 | 102 | return NewFilterNode("drawtext", []*Stream{s}, 1, nil, args).Stream("", "") 103 | } 104 | 105 | func Concat(streams []*Stream, kwargs ...KwArgs) *Stream { 106 | args := MergeKwArgs(kwargs) 107 | vsc := args.GetDefault("v", 1).(int) 108 | asc := args.GetDefault("a", 0).(int) 109 | sc := vsc + asc 110 | if len(streams)%sc != 0 { 111 | panic("streams count not valid") 112 | } 113 | args["n"] = len(streams) / sc 114 | return NewFilterNode("concat", streams, -1, nil, args).Stream("", "") 115 | } 116 | 117 | func (s *Stream) Concat(streams []*Stream, kwargs ...KwArgs) *Stream { 118 | return Concat(append(streams, s), MergeKwArgs(kwargs)) 119 | } 120 | 121 | func (s *Stream) ZoomPan(kwargs ...KwArgs) *Stream { 122 | AssertType(s.Type, "FilterableStream", "zoompan") 123 | return NewFilterNode("zoompan", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") 124 | } 125 | 126 | func (s *Stream) Hue(kwargs ...KwArgs) *Stream { 127 | AssertType(s.Type, "FilterableStream", "hue") 128 | return NewFilterNode("hue", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") 129 | } 130 | 131 | // todo fix this 132 | func (s *Stream) ColorChannelMixer(kwargs ...KwArgs) *Stream { 133 | AssertType(s.Type, "FilterableStream", "colorchannelmixer") 134 | return NewFilterNode("colorchannelmixer", []*Stream{s}, 1, nil, MergeKwArgs(kwargs)).Stream("", "") 135 | } 136 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "sort" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/u2takey/go-utils/sets" 11 | ) 12 | 13 | func getString(item interface{}) string { 14 | if a, ok := item.(interface{ String() string }); ok { 15 | return a.String() 16 | } 17 | switch a := item.(type) { 18 | case string: 19 | return a 20 | case []string: 21 | return strings.Join(a, ", ") 22 | case Args: 23 | return strings.Join(a, ", ") 24 | case []interface{}: 25 | var r []string 26 | for _, b := range a { 27 | r = append(r, getString(b)) 28 | } 29 | return strings.Join(r, ", ") 30 | case KwArgs: 31 | var keys, r []string 32 | for k := range a { 33 | keys = append(keys, k) 34 | } 35 | sort.Strings(keys) 36 | for _, k := range keys { 37 | r = append(r, fmt.Sprintf("%s: %s", k, getString(a[k]))) 38 | } 39 | return fmt.Sprintf("{%s}", strings.Join(r, ", ")) 40 | case map[string]interface{}: 41 | var keys, r []string 42 | for k := range a { 43 | keys = append(keys, k) 44 | } 45 | sort.Strings(keys) 46 | for _, k := range keys { 47 | r = append(r, fmt.Sprintf("%s: %s", k, getString(a[k]))) 48 | } 49 | return fmt.Sprintf("{%s}", strings.Join(r, ", ")) 50 | } 51 | return fmt.Sprintf("%v", item) 52 | } 53 | 54 | func getHash(item interface{}) int { 55 | h := fnv.New64() 56 | switch a := item.(type) { 57 | case interface{ Hash() int }: 58 | return a.Hash() 59 | case string: 60 | _, _ = h.Write([]byte(a)) 61 | return int(h.Sum64()) 62 | case []byte: 63 | _, _ = h.Write(a) 64 | return int(h.Sum64()) 65 | case map[string]interface{}: 66 | b := 0 67 | for k, v := range a { 68 | b += getHash(k) + getHash(v) 69 | } 70 | return b 71 | case KwArgs: 72 | b := 0 73 | for k, v := range a { 74 | b += getHash(k) + getHash(v) 75 | } 76 | return b 77 | default: 78 | _, _ = h.Write([]byte(getString(item))) 79 | return int(h.Sum64()) 80 | } 81 | } 82 | 83 | func escapeChars(text, chars string) string { 84 | s := sets.NewString() 85 | for _, a := range chars { 86 | s.Insert(string(a)) 87 | } 88 | sl := s.List() 89 | if s.Has("\\") { 90 | s.Delete("\\") 91 | sl = append([]string{"\\"}, s.List()...) 92 | } 93 | for _, ch := range sl { 94 | text = strings.ReplaceAll(text, ch, "\\"+ch) 95 | } 96 | return text 97 | } 98 | 99 | type Args []string 100 | 101 | func (a Args) Sorted() Args { 102 | sort.Strings(a) 103 | return a 104 | } 105 | 106 | func (a Args) EscapeWith(chars string) Args { 107 | out := Args{} 108 | for _, b := range a { 109 | out = append(out, escapeChars(b, chars)) 110 | } 111 | return out 112 | } 113 | 114 | type KwArgs map[string]interface{} 115 | 116 | func MergeKwArgs(args []KwArgs) KwArgs { 117 | a := KwArgs{} 118 | for _, b := range args { 119 | for c := range b { 120 | a[c] = b[c] 121 | } 122 | } 123 | return a 124 | } 125 | 126 | func (a KwArgs) EscapeWith(chars string) KwArgs { 127 | out := KwArgs{} 128 | for k, v := range a { 129 | out[escapeChars(k, chars)] = escapeChars(getString(v), chars) 130 | } 131 | return out 132 | } 133 | 134 | func (a KwArgs) Copy() KwArgs { 135 | r := KwArgs{} 136 | for k := range a { 137 | r[k] = a[k] 138 | } 139 | return r 140 | } 141 | 142 | func (a KwArgs) SortedKeys() []string { 143 | var r []string 144 | for k := range a { 145 | r = append(r, k) 146 | } 147 | sort.Strings(r) 148 | return r 149 | } 150 | 151 | func (a KwArgs) GetString(k string) string { 152 | if v, ok := a[k]; ok { 153 | return fmt.Sprintf("%v", v) 154 | } 155 | return "" 156 | } 157 | 158 | func (a KwArgs) PopString(k string) string { 159 | if c, ok := a[k]; ok { 160 | defer delete(a, k) 161 | return fmt.Sprintf("%v", c) 162 | } 163 | return "" 164 | } 165 | 166 | func (a KwArgs) HasKey(k string) bool { 167 | _, ok := a[k] 168 | return ok 169 | } 170 | 171 | func (a KwArgs) GetDefault(k string, defaultV interface{}) interface{} { 172 | if v, ok := a[k]; ok { 173 | return v 174 | } 175 | return defaultV 176 | } 177 | 178 | func (a KwArgs) PopDefault(k string, defaultV interface{}) interface{} { 179 | if v, ok := a[k]; ok { 180 | defer delete(a, k) 181 | return v 182 | } 183 | return defaultV 184 | } 185 | 186 | func ConvertKwargsToCmdLineArgs(kwargs KwArgs) []string { 187 | var keys, args []string 188 | for k := range kwargs { 189 | keys = append(keys, k) 190 | } 191 | sort.Strings(keys) 192 | 193 | for _, k := range keys { 194 | v := kwargs[k] 195 | switch a := v.(type) { 196 | case string: 197 | args = append(args, fmt.Sprintf("-%s", k)) 198 | if a != "" { 199 | args = append(args, a) 200 | } 201 | case []string: 202 | for _, r := range a { 203 | args = append(args, fmt.Sprintf("-%s", k)) 204 | if r != "" { 205 | args = append(args, r) 206 | } 207 | } 208 | case []int: 209 | for _, r := range a { 210 | args = append(args, fmt.Sprintf("-%s", k)) 211 | args = append(args, strconv.Itoa(r)) 212 | } 213 | case int: 214 | args = append(args, fmt.Sprintf("-%s", k)) 215 | args = append(args, strconv.Itoa(a)) 216 | default: 217 | args = append(args, fmt.Sprintf("-%s", k)) 218 | args = append(args, fmt.Sprintf("%v", a)) 219 | } 220 | } 221 | return args 222 | } 223 | -------------------------------------------------------------------------------- /ffmpeg.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go/aws" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 14 | ) 15 | 16 | // Input file URL (ffmpeg “-i“ option) 17 | // 18 | // Any supplied kwargs are passed to ffmpeg verbatim (e.g. “t=20“, 19 | // “f='mp4'“, “acodec='pcm'“, etc.). 20 | // 21 | // To tell ffmpeg to read from stdin, use “pipe:“ as the filename. 22 | // 23 | // Official documentation: `Main options `__ 24 | func Input(filename string, kwargs ...KwArgs) *Stream { 25 | args := MergeKwArgs(kwargs) 26 | args["filename"] = filename 27 | if fmt := args.PopString("f"); fmt != "" { 28 | if args.HasKey("format") { 29 | panic(errors.New("can't specify both `format` and `f` options")) 30 | } 31 | args["format"] = fmt 32 | } 33 | return NewInputNode("input", nil, args).Stream("", "") 34 | } 35 | 36 | // Add extra global command-line argument(s), e.g. “-progress“. 37 | func (s *Stream) GlobalArgs(args ...string) *Stream { 38 | if s.Type != "OutputStream" { 39 | panic("cannot overwrite outputs on non-OutputStream") 40 | } 41 | return NewGlobalNode("global_args", []*Stream{s}, args, nil).Stream("", "") 42 | } 43 | 44 | // Overwrite output files without asking (ffmpeg “-y“ option) 45 | // 46 | // Official documentation: `Main options `_ 47 | func (s *Stream) OverwriteOutput(stream *Stream) *Stream { 48 | if s.Type != "OutputStream" { 49 | panic("cannot overwrite outputs on non-OutputStream") 50 | } 51 | return NewGlobalNode("overwrite_output", []*Stream{stream}, []string{"-y"}, nil).Stream("", "") 52 | } 53 | 54 | // Include all given outputs in one ffmpeg command line 55 | func MergeOutputs(streams ...*Stream) *Stream { 56 | return NewMergeOutputsNode("merge_output", streams).Stream("", "") 57 | } 58 | 59 | // Output file URL 60 | // 61 | // Syntax: 62 | // `ffmpeg.Output([]*Stream{stream1, stream2, stream3...}, filename, kwargs)` 63 | // 64 | // Any supplied keyword arguments are passed to ffmpeg verbatim (e.g. 65 | // ``t=20``, ``f='mp4'``, ``acodec='pcm'``, ``vcodec='rawvideo'``, 66 | // etc.). Some keyword-arguments are handled specially, as shown below. 67 | // 68 | // Args: 69 | // video_bitrate: parameter for ``-b:v``, e.g. ``video_bitrate=1000``. 70 | // audio_bitrate: parameter for ``-b:a``, e.g. ``audio_bitrate=200``. 71 | // format: alias for ``-f`` parameter, e.g. ``format='mp4'`` 72 | // (equivalent to ``f='mp4'``). 73 | // 74 | // If multiple streams are provided, they are mapped to the same 75 | // output. 76 | // 77 | // To tell ffmpeg to write to stdout, use ``pipe:`` as the filename. 78 | // 79 | // Official documentation: `Synopsis `__ 80 | // """ 81 | func Output(streams []*Stream, fileName string, kwargs ...KwArgs) *Stream { 82 | args := MergeKwArgs(kwargs) 83 | if !args.HasKey("filename") { 84 | if fileName == "" { 85 | panic("filename must be provided") 86 | } 87 | args["filename"] = fileName 88 | } 89 | 90 | return NewOutputNode("output", streams, nil, args).Stream("", "") 91 | } 92 | 93 | // Output file URL 94 | // 95 | // Syntax: 96 | // `ffmpeg.Output(ctx, []*Stream{stream1, stream2, stream3...}, filename, kwargs)` 97 | // 98 | // Any supplied keyword arguments are passed to ffmpeg verbatim (e.g. 99 | // ``t=20``, ``f='mp4'``, ``acodec='pcm'``, ``vcodec='rawvideo'``, 100 | // etc.). Some keyword-arguments are handled specially, as shown below. 101 | // 102 | // Args: 103 | // video_bitrate: parameter for ``-b:v``, e.g. ``video_bitrate=1000``. 104 | // audio_bitrate: parameter for ``-b:a``, e.g. ``audio_bitrate=200``. 105 | // format: alias for ``-f`` parameter, e.g. ``format='mp4'`` 106 | // (equivalent to ``f='mp4'``). 107 | // 108 | // If multiple streams are provided, they are mapped to the same 109 | // output. 110 | // 111 | // To tell ffmpeg to write to stdout, use ``pipe:`` as the filename. 112 | // 113 | // Official documentation: `Synopsis `__ 114 | // """ 115 | func OutputContext(ctx context.Context, streams []*Stream, fileName string, kwargs ...KwArgs) *Stream { 116 | output := Output(streams, fileName, kwargs...) 117 | output.Context = ctx 118 | return output 119 | } 120 | 121 | func (s *Stream) Output(fileName string, kwargs ...KwArgs) *Stream { 122 | if s.Type != "FilterableStream" { 123 | log.Panic("cannot output on non-FilterableStream") 124 | } 125 | if strings.HasPrefix(fileName, "s3://") { 126 | return s.outputS3Stream(fileName, kwargs...) 127 | } 128 | return OutputContext(s.Context, []*Stream{s}, fileName, kwargs...) 129 | } 130 | 131 | func (s *Stream) outputS3Stream(fileName string, kwargs ...KwArgs) *Stream { 132 | r, w := io.Pipe() 133 | fileL := strings.SplitN(strings.TrimPrefix(fileName, "s3://"), "/", 2) 134 | if len(fileL) != 2 { 135 | log.Panic("s3 file format not valid") 136 | } 137 | args := MergeKwArgs(kwargs) 138 | awsConfig := args.PopDefault("aws_config", &aws.Config{}).(*aws.Config) 139 | bucket, key := fileL[0], fileL[1] 140 | o := Output([]*Stream{s}, "pipe:", args). 141 | WithOutput(w, os.Stdout) 142 | done := make(chan struct{}) 143 | runHook := RunHook{ 144 | f: func() { 145 | defer func() { 146 | done <- struct{}{} 147 | }() 148 | 149 | sess, err := session.NewSession(awsConfig) 150 | uploader := s3manager.NewUploader(sess) 151 | _, err = uploader.Upload(&s3manager.UploadInput{ 152 | Bucket: &bucket, 153 | Key: &key, 154 | Body: r, 155 | }) 156 | //fmt.Println(ioutil.ReadAll(r)) 157 | if err != nil { 158 | log.Println("upload fail", err) 159 | } 160 | }, 161 | done: done, 162 | closer: w, 163 | } 164 | o.Context = context.WithValue(o.Context, "run_hook", &runHook) 165 | return o 166 | } 167 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go v1.38.20 h1:QbzNx/tdfATbdKfubBpkt84OM6oBkxQZRw6+bW2GyeA= 2 | github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= 7 | github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 10 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 11 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 12 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 14 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 15 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 16 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 17 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 18 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 19 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 20 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 23 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 24 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 25 | github.com/panjf2000/ants/v2 v2.4.2/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= 26 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 27 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 31 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 32 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 33 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 34 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 35 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 36 | github.com/u2takey/go-utils v0.3.1 h1:TaQTgmEZZeDHQFYfd+AdUT1cT4QJgJn/XVPELhHw4ys= 37 | github.com/u2takey/go-utils v0.3.1/go.mod h1:6e+v5vEZ/6gu12w/DC2ixZdZtCrNokVxD0JUklcqdCs= 38 | gocv.io/x/gocv v0.25.0 h1:vM50jL3v9OEqWSi+urelX5M1ptZeFWA/VhGPvdTqsJU= 39 | gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= 40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 41 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 42 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= 43 | golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 44 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 45 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 46 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 47 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 48 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 49 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 52 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 53 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 54 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 55 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 58 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 59 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 61 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 62 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 63 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 64 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 65 | -------------------------------------------------------------------------------- /dag.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Node in a directed-acyclic graph (DAG). 8 | // 9 | // Edges: 10 | // DagNodes are connected by edges. An edge connects two nodes with a label for each side: 11 | // - ``upstream_node``: upstream/parent node 12 | // - ``upstream_label``: label on the outgoing side of the upstream node 13 | // - ``downstream_node``: downstream/child node 14 | // - ``downstream_label``: label on the incoming side of the downstream node 15 | // 16 | // For example, DagNode A may be connected to DagNode B with an edge labelled "foo" on A's side, and "bar" on B's 17 | // side: 18 | // 19 | // _____ _____ 20 | // | | | | 21 | // | A >[foo]---[bar]> B | 22 | // |_____| |_____| 23 | // 24 | // Edge labels may be integers or strings, and nodes cannot have more than one incoming edge with the same label. 25 | // 26 | // DagNodes may have any number of incoming edges and any number of outgoing edges. DagNodes keep track only of 27 | // their incoming edges, but the entire graph structure can be inferred by looking at the furthest downstream 28 | // nodes and working backwards. 29 | // 30 | // Hashing: 31 | // DagNodes must be hashable, and two nodes are considered to be equivalent if they have the same hash value. 32 | // 33 | // Nodes are immutable, and the hash should remain constant as a result. If a node with new contents is required, 34 | // create a new node and throw the old one away. 35 | // 36 | // String representation: 37 | // In order for graph visualization tools to show useful information, nodes must be representable as strings. The 38 | // ``String`` operator should provide a more or less "full" representation of the node, and the ``ShortRepr`` 39 | // property should be a shortened, concise representation. 40 | // 41 | // Again, because nodes are immutable, the string representations should remain constant. 42 | type DagNode interface { 43 | Hash() int 44 | // Compare two nodes 45 | Equal(other DagNode) bool 46 | // Return a full string representation of the node. 47 | String() string 48 | // Return a partial/concise representation of the node 49 | ShortRepr() string 50 | // Provides information about all incoming edges that connect to this node. 51 | // 52 | // The edge map is a dictionary that maps an ``incoming_label`` to ``(outgoing_node, outgoing_label)``. Note that 53 | // implicity, ``incoming_node`` is ``self``. See "Edges" section above. 54 | IncomingEdgeMap() map[Label]NodeInfo 55 | } 56 | 57 | type Label string 58 | type NodeInfo struct { 59 | Node DagNode 60 | Label Label 61 | Selector Selector 62 | } 63 | type Selector string 64 | 65 | type DagEdge struct { 66 | DownStreamNode DagNode 67 | DownStreamLabel Label 68 | UpStreamNode DagNode 69 | UpStreamLabel Label 70 | UpStreamSelector Selector 71 | } 72 | 73 | func GetInComingEdges(downStreamNode DagNode, inComingEdgeMap map[Label]NodeInfo) []DagEdge { 74 | var edges []DagEdge 75 | for _, downStreamLabel := range _getAllLabelsInSorted(inComingEdgeMap) { 76 | upStreamInfo := inComingEdgeMap[downStreamLabel] 77 | edges = append(edges, DagEdge{ 78 | DownStreamNode: downStreamNode, 79 | DownStreamLabel: downStreamLabel, 80 | UpStreamNode: upStreamInfo.Node, 81 | UpStreamLabel: upStreamInfo.Label, 82 | UpStreamSelector: upStreamInfo.Selector, 83 | }) 84 | } 85 | return edges 86 | } 87 | 88 | func GetOutGoingEdges(upStreamNode DagNode, outOutingEdgeMap map[Label][]NodeInfo) []DagEdge { 89 | var edges []DagEdge 90 | for _, upStreamLabel := range _getAllLabelsSorted(outOutingEdgeMap) { 91 | downStreamInfos := outOutingEdgeMap[upStreamLabel] 92 | for _, downStreamInfo := range downStreamInfos { 93 | edges = append(edges, DagEdge{ 94 | DownStreamNode: downStreamInfo.Node, 95 | DownStreamLabel: downStreamInfo.Label, 96 | UpStreamNode: upStreamNode, 97 | UpStreamLabel: upStreamLabel, 98 | UpStreamSelector: downStreamInfo.Selector, 99 | }) 100 | } 101 | 102 | } 103 | return edges 104 | } 105 | 106 | func TopSort(downStreamNodes []DagNode) (sortedNodes []DagNode, outOutingEdgeMaps map[int]map[Label][]NodeInfo, err error) { 107 | markedNodes := map[int]struct{}{} 108 | markedSortedNodes := map[int]struct{}{} 109 | outOutingEdgeMaps = map[int]map[Label][]NodeInfo{} 110 | 111 | var visit func(upStreamNode, downstreamNode DagNode, upStreamLabel, downStreamLabel Label, downStreamSelector Selector) error 112 | visit = func(upStreamNode, downstreamNode DagNode, upStreamLabel, downStreamLabel Label, downStreamSelector Selector) error { 113 | if _, ok := markedNodes[upStreamNode.Hash()]; ok { 114 | return errors.New("graph if not DAG") 115 | } 116 | if downstreamNode != nil { 117 | if a, ok := outOutingEdgeMaps[upStreamNode.Hash()]; !ok || a == nil { 118 | outOutingEdgeMaps[upStreamNode.Hash()] = map[Label][]NodeInfo{} 119 | } 120 | outgoingEdgeMap := outOutingEdgeMaps[upStreamNode.Hash()] 121 | outgoingEdgeMap[upStreamLabel] = append(outgoingEdgeMap[upStreamLabel], NodeInfo{ 122 | Node: downstreamNode, 123 | Label: downStreamLabel, 124 | Selector: downStreamSelector, 125 | }) 126 | } 127 | 128 | if _, ok := markedSortedNodes[upStreamNode.Hash()]; !ok { 129 | markedNodes[upStreamNode.Hash()] = struct{}{} 130 | for _, edge := range GetInComingEdges(upStreamNode, upStreamNode.IncomingEdgeMap()) { 131 | err := visit(edge.UpStreamNode, edge.DownStreamNode, edge.UpStreamLabel, edge.DownStreamLabel, edge.UpStreamSelector) 132 | if err != nil { 133 | return err 134 | } 135 | } 136 | delete(markedNodes, upStreamNode.Hash()) 137 | sortedNodes = append(sortedNodes, upStreamNode) 138 | markedSortedNodes[upStreamNode.Hash()] = struct{}{} 139 | } 140 | return nil 141 | } 142 | 143 | for len(downStreamNodes) > 0 { 144 | node := downStreamNodes[len(downStreamNodes)-1] 145 | downStreamNodes = downStreamNodes[:len(downStreamNodes)-1] 146 | err = visit(node, nil, "", "", "") 147 | if err != nil { 148 | return 149 | } 150 | } 151 | return 152 | } 153 | -------------------------------------------------------------------------------- /examples/opencv_test.go: -------------------------------------------------------------------------------- 1 | // +build gocv 2 | 3 | // uncomment line above for gocv examples 4 | 5 | package examples 6 | 7 | import ( 8 | "fmt" 9 | "image" 10 | "image/color" 11 | "io" 12 | "log" 13 | "testing" 14 | 15 | ffmpeg "github.com/u2takey/ffmpeg-go" 16 | "gocv.io/x/gocv" 17 | ) 18 | 19 | // TestExampleOpenCvFaceDetect will: take a video as input => use opencv for face detection => draw box and show a window 20 | // This example depends on gocv and opencv, please refer: https://pkg.go.dev/gocv.io/x/gocv for installation. 21 | func TestExampleOpenCvFaceDetectWithVideo(t *testing.T) { 22 | inputFile := "./sample_data/head-pose-face-detection-male-short.mp4" 23 | xmlFile := "./sample_data/haarcascade_frontalface_default.xml" 24 | 25 | w, h := getVideoSize(inputFile) 26 | log.Println(w, h) 27 | 28 | pr1, pw1 := io.Pipe() 29 | readProcess(inputFile, pw1) 30 | openCvProcess(xmlFile, pr1, w, h) 31 | log.Println("Done") 32 | } 33 | 34 | func readProcess(infileName string, writer io.WriteCloser) { 35 | log.Println("Starting ffmpeg process1") 36 | go func() { 37 | err := ffmpeg.Input(infileName). 38 | Output("pipe:", 39 | ffmpeg.KwArgs{ 40 | "format": "rawvideo", "pix_fmt": "rgb24", 41 | }). 42 | WithOutput(writer). 43 | ErrorToStdOut(). 44 | Run() 45 | log.Println("ffmpeg process1 done") 46 | _ = writer.Close() 47 | if err != nil { 48 | panic(err) 49 | } 50 | }() 51 | return 52 | } 53 | 54 | func openCvProcess(xmlFile string, reader io.ReadCloser, w, h int) { 55 | // open display window 56 | window := gocv.NewWindow("Face Detect") 57 | defer window.Close() 58 | 59 | // color for the rect when faces detected 60 | blue := color.RGBA{B: 255} 61 | 62 | classifier := gocv.NewCascadeClassifier() 63 | defer classifier.Close() 64 | 65 | if !classifier.Load(xmlFile) { 66 | fmt.Printf("Error reading cascade file: %v\n", xmlFile) 67 | return 68 | } 69 | 70 | frameSize := w * h * 3 71 | buf := make([]byte, frameSize, frameSize) 72 | for { 73 | n, err := io.ReadFull(reader, buf) 74 | if n == 0 || err == io.EOF { 75 | return 76 | } else if n != frameSize || err != nil { 77 | panic(fmt.Sprintf("read error: %d, %s", n, err)) 78 | } 79 | img, err := gocv.NewMatFromBytes(h, w, gocv.MatTypeCV8UC3, buf) 80 | if err != nil { 81 | fmt.Println("decode fail", err) 82 | } 83 | if img.Empty() { 84 | continue 85 | } 86 | img2 := gocv.NewMat() 87 | gocv.CvtColor(img, &img2, gocv.ColorBGRToRGB) 88 | 89 | // detect faces 90 | rects := classifier.DetectMultiScale(img2) 91 | fmt.Printf("found %d faces\n", len(rects)) 92 | 93 | // draw a rectangle around each face on the original image, along with text identifing as "Human" 94 | for _, r := range rects { 95 | gocv.Rectangle(&img2, r, blue, 3) 96 | 97 | size := gocv.GetTextSize("Human", gocv.FontHersheyPlain, 1.2, 2) 98 | pt := image.Pt(r.Min.X+(r.Min.X/2)-(size.X/2), r.Min.Y-2) 99 | gocv.PutText(&img2, "Human", pt, gocv.FontHersheyPlain, 1.2, blue, 2) 100 | } 101 | 102 | // show the image in the window, and wait 1 millisecond 103 | window.IMShow(img2) 104 | img.Close() 105 | img2.Close() 106 | if window.WaitKey(10) >= 0 { 107 | break 108 | } 109 | } 110 | return 111 | } 112 | 113 | // TestExampleOpenCvFaceDetectWithCamera will: task stream from webcam => use opencv for face detection => output with ffmpeg 114 | // This example depends on gocv and opencv, please refer: https://pkg.go.dev/gocv.io/x/gocv for installation. 115 | func TestExampleOpenCvFaceDetectWithCamera(t *testing.T) { 116 | deviceID := "0" // camera device id 117 | xmlFile := "./sample_data/haarcascade_frontalface_default.xml" 118 | 119 | webcam, err := gocv.OpenVideoCapture(deviceID) 120 | if err != nil { 121 | fmt.Printf("error opening video capture device: %v\n", deviceID) 122 | return 123 | } 124 | defer webcam.Close() 125 | 126 | // prepare image matrix 127 | img := gocv.NewMat() 128 | defer img.Close() 129 | 130 | if ok := webcam.Read(&img); !ok { 131 | panic(fmt.Sprintf("Cannot read device %v", deviceID)) 132 | } 133 | fmt.Printf("img: %vX%v\n", img.Cols(), img.Rows()) 134 | 135 | pr1, pw1 := io.Pipe() 136 | writeProcess("./sample_data/face_detect.mp4", pr1, img.Cols(), img.Rows()) 137 | 138 | // color for the rect when faces detected 139 | blue := color.RGBA{B: 255} 140 | 141 | // load classifier to recognize faces 142 | classifier := gocv.NewCascadeClassifier() 143 | defer classifier.Close() 144 | 145 | if !classifier.Load(xmlFile) { 146 | fmt.Printf("Error reading cascade file: %v\n", xmlFile) 147 | return 148 | } 149 | 150 | fmt.Printf("Start reading device: %v\n", deviceID) 151 | for i := 0; i < 200; i++ { 152 | if ok := webcam.Read(&img); !ok { 153 | fmt.Printf("Device closed: %v\n", deviceID) 154 | return 155 | } 156 | if img.Empty() { 157 | continue 158 | } 159 | 160 | // detect faces 161 | rects := classifier.DetectMultiScale(img) 162 | fmt.Printf("found %d faces\n", len(rects)) 163 | 164 | // draw a rectangle around each face on the original image, along with text identifing as "Human" 165 | for _, r := range rects { 166 | gocv.Rectangle(&img, r, blue, 3) 167 | 168 | size := gocv.GetTextSize("Human", gocv.FontHersheyPlain, 1.2, 2) 169 | pt := image.Pt(r.Min.X+(r.Min.X/2)-(size.X/2), r.Min.Y-2) 170 | gocv.PutText(&img, "Human", pt, gocv.FontHersheyPlain, 1.2, blue, 2) 171 | } 172 | pw1.Write(img.ToBytes()) 173 | } 174 | pw1.Close() 175 | log.Println("Done") 176 | } 177 | 178 | func writeProcess(outputFile string, reader io.ReadCloser, w, h int) { 179 | log.Println("Starting ffmpeg process1") 180 | go func() { 181 | err := ffmpeg.Input("pipe:", 182 | ffmpeg.KwArgs{"format": "rawvideo", 183 | "pix_fmt": "bgr24", "s": fmt.Sprintf("%dx%d", w, h), 184 | }). 185 | Overlay(ffmpeg.Input("./sample_data/overlay.png"), ""). 186 | Output(outputFile). 187 | WithInput(reader). 188 | ErrorToStdOut(). 189 | OverWriteOutput(). 190 | Run() 191 | log.Println("ffmpeg process1 done") 192 | if err != nil { 193 | panic(err) 194 | } 195 | _ = reader.Close() 196 | }() 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ffmpeg-go 2 | 3 | ffmpeg-go is golang port of https://github.com/kkroening/ffmpeg-python 4 | 5 | check examples/example_test.go and ffmpeg_test.go for more examples. 6 | 7 | # How to get and use 8 | You can get this package via: 9 | ``` 10 | go get -u github.com/u2takey/ffmpeg-go 11 | ``` 12 | 13 | > **Note**: `ffmpeg-go` makes no attempt to download/install FFmpeg, as `ffmpeg-go` is merely a pure-Go wrapper - whereas FFmpeg installation is platform-dependent/environment-specific, and is thus the responsibility of the user, as described below. 14 | 15 | ### Installing FFmpeg 16 | 17 | Before using `ffmpeg-go`, FFmpeg must be installed and accessible via the `$PATH` environment variable. 18 | 19 | There are a variety of ways to install FFmpeg, such as the [official download links](https://ffmpeg.org/download.html), or using your package manager of choice (e.g. `sudo apt install ffmpeg` on Debian/Ubuntu, `brew install ffmpeg` on OS X, etc.). 20 | 21 | Regardless of how FFmpeg is installed, you can check if your environment path is set correctly by running the `ffmpeg` command from the terminal, in which case the version information should appear, as in the following example (truncated for brevity): 22 | 23 | ``` 24 | $ ffmpeg 25 | ffmpeg version 4.2.4-1ubuntu0.1 Copyright (c) 2000-2020 the FFmpeg developers 26 | built with gcc 9 (Ubuntu 9.3.0-10ubuntu2) 27 | ``` 28 | 29 | > **Note**: The actual version information displayed here may vary from one system to another; but if a message such as `ffmpeg: command not found` appears instead of the version information, FFmpeg is not properly installed. 30 | 31 | # Examples 32 | 33 | ```go 34 | split := Input(TestInputFile1).VFlip().Split() 35 | split0, split1 := split.Get("0"), split.Get("1") 36 | overlayFile := Input(TestOverlayFile).Crop(10, 10, 158, 112) 37 | err := Concat([]*Stream{ 38 | split0.Trim(KwArgs{"start_frame": 10, "end_frame": 20}), 39 | split1.Trim(KwArgs{"start_frame": 30, "end_frame": 40})}). 40 | Overlay(overlayFile.HFlip(), ""). 41 | DrawBox(50, 50, 120, 120, "red", 5). 42 | Output(TestOutputFile1). 43 | OverWriteOutput(). 44 | Run() 45 | ``` 46 | 47 | ## Transcoding From One Codec To Another 48 | 49 | ```go 50 | err := ffmpeg.Input("./sample_data/in1.mp4"). 51 | Output("./sample_data/out1.mp4", ffmpeg.KwArgs{"c:v": "libx265"}). 52 | OverWriteOutput().ErrorToStdOut().Run() 53 | ``` 54 | 55 | ## Cut Video From Timestamp 56 | 57 | ```go 58 | err := ffmpeg.Input("./sample_data/in1.mp4", ffmpeg.KwArgs{"ss": 1}). 59 | Output("./sample_data/out1.mp4", ffmpeg.KwArgs{"t": 1}).OverWriteOutput().Run() 60 | assert.Nil(t, err) 61 | ``` 62 | 63 | ## Add Watermark For Video 64 | ```go 65 | // show watermark with size 64:-1 in the top left corner after seconds 1 66 | overlay := ffmpeg.Input("./sample_data/overlay.png").Filter("scale", ffmpeg.Args{"64:-1"}) 67 | err := ffmpeg.Filter( 68 | []*ffmpeg.Stream{ 69 | ffmpeg.Input("./sample_data/in1.mp4"), 70 | overlay, 71 | }, "overlay", ffmpeg.Args{"10:10"}, ffmpeg.KwArgs{"enable": "gte(t,1)"}). 72 | Output("./sample_data/out1.mp4").OverWriteOutput().ErrorToStdOut().Run() 73 | ``` 74 | 75 | result: 76 | 77 | ![img.png](./docs/example_overlay.png) 78 | 79 | ## Cut Video For Gif 80 | 81 | ```go 82 | err := ffmpeg.Input("./sample_data/in1.mp4", ffmpeg.KwArgs{"ss": "1"}). 83 | Output("./sample_data/out1.gif", ffmpeg.KwArgs{"s": "320x240", "pix_fmt": "rgb24", "t": "3", "r": "3"}). 84 | OverWriteOutput().ErrorToStdOut().Run() 85 | ``` 86 | 87 | result: 88 | 89 | ![img.png](./docs/example_gif.gif) 90 | 91 | ## Task Frame From Video 92 | 93 | ```bash 94 | func ExampleReadFrameAsJpeg(inFileName string, frameNum int) io.Reader { 95 | buf := bytes.NewBuffer(nil) 96 | err := ffmpeg.Input(inFileName). 97 | Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}). 98 | Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). 99 | WithOutput(buf, os.Stdout). 100 | Run() 101 | if err != nil { 102 | panic(err) 103 | } 104 | return buf 105 | } 106 | 107 | reader := ExampleReadFrameAsJpeg("./sample_data/in1.mp4", 5) 108 | img, err := imaging.Decode(reader) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | err = imaging.Save(img, "./sample_data/out1.jpeg") 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | ``` 117 | result : 118 | 119 | ![image](./examples/sample_data/out1.jpeg) 120 | 121 | ## Get Multiple Output 122 | 123 | ```go 124 | // get multiple output with different size/bitrate 125 | input := ffmpeg.Input("./sample_data/in1.mp4").Split() 126 | out1 := input.Get("0").Filter("scale", ffmpeg.Args{"1920:-1"}). 127 | Output("./sample_data/1920.mp4", ffmpeg.KwArgs{"b:v": "5000k"}) 128 | out2 := input.Get("1").Filter("scale", ffmpeg.Args{"1280:-1"}). 129 | Output("./sample_data/1280.mp4", ffmpeg.KwArgs{"b:v": "2800k"}) 130 | 131 | err := ffmpeg.MergeOutputs(out1, out2).OverWriteOutput().ErrorToStdOut().Run() 132 | ``` 133 | 134 | ## Show FFmpeg Progress 135 | 136 | see complete example at: [showProgress](./examples/showProgress.go) 137 | 138 | ```bash 139 | func ExampleShowProgress(inFileName, outFileName string) { 140 | a, err := ffmpeg.Probe(inFileName) 141 | if err != nil { 142 | panic(err) 143 | } 144 | totalDuration := gjson.Get(a, "format.duration").Float() 145 | 146 | err = ffmpeg.Input(inFileName). 147 | Output(outFileName, ffmpeg.KwArgs{"c:v": "libx264", "preset": "veryslow"}). 148 | GlobalArgs("-progress", "unix://"+TempSock(totalDuration)). 149 | OverWriteOutput(). 150 | Run() 151 | if err != nil { 152 | panic(err) 153 | } 154 | } 155 | ExampleShowProgress("./sample_data/in1.mp4", "./sample_data/out2.mp4") 156 | ``` 157 | 158 | result 159 | 160 | ```bash 161 | progress: .0 162 | progress: 0.72 163 | progress: 1.00 164 | progress: done 165 | ``` 166 | 167 | ## Integrate FFmpeg-go With Open-CV (gocv) For Face-detect 168 | 169 | see complete example at: [opencv](./examples/opencv_test.go) 170 | 171 | result: ![image](./examples/sample_data/face-detect.jpg) 172 | 173 | ## Set Cpu limit/request For FFmpeg-go 174 | 175 | ```go 176 | e := ComplexFilterExample("./sample_data/in1.mp4", "./sample_data/overlay.png", "./sample_data/out2.mp4") 177 | err := e.RunWithResource(0.1, 0.5) 178 | if err != nil { 179 | assert.Nil(t, err) 180 | } 181 | ``` 182 | 183 | result from command top: we will see ffmpeg used 0.5 core as expected. 184 | 185 | ```bash 186 | > top 187 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 188 | 1386105 root 20 0 2114152 273780 31672 R 50.2 1.7 0:16.79 ffmpeg 189 | ``` 190 | 191 | # View Progress Graph 192 | 193 | function view generate [mermaid](https://mermaid-js.github.io/mermaid/#/) chart, which can be use in markdown or view [online](https://mermaid-js.github.io/mermaid-live-editor/) 194 | 195 | ```go 196 | split := Input(TestInputFile1).VFlip().Split() 197 | split0, split1 := split.Get("0"), split.Get("1") 198 | overlayFile := Input(TestOverlayFile).Crop(10, 10, 158, 112) 199 | b, err := Concat([]*Stream{ 200 | split0.Trim(KwArgs{"start_frame": 10, "end_frame": 20}), 201 | split1.Trim(KwArgs{"start_frame": 30, "end_frame": 40})}). 202 | Overlay(overlayFile.HFlip(), ""). 203 | DrawBox(50, 50, 120, 120, "red", 5). 204 | Output(TestOutputFile1). 205 | OverWriteOutput().View(ViewTypeFlowChart) 206 | fmt.Println(b) 207 | ``` 208 | ![image](./docs/flowchart2.png) 209 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/u2takey/go-utils/sets" 10 | ) 11 | 12 | type Stream struct { 13 | Node *Node 14 | Label Label 15 | Selector Selector 16 | Type string 17 | FfmpegPath string 18 | Context context.Context 19 | } 20 | 21 | type RunHook struct { 22 | f func() 23 | done <-chan struct{} 24 | closer interface { 25 | Close() error 26 | } 27 | } 28 | 29 | func NewStream(node *Node, streamType string, label Label, selector Selector) *Stream { 30 | return &Stream{ 31 | Node: node, 32 | Label: label, 33 | Selector: selector, 34 | Type: streamType, 35 | FfmpegPath: "ffmpeg", 36 | Context: context.Background(), 37 | } 38 | } 39 | 40 | func (s *Stream) Hash() int { 41 | return s.Node.Hash() + getHash(s.Label) 42 | } 43 | 44 | func (s *Stream) Equal(other Stream) bool { 45 | return s.Hash() == other.Hash() 46 | } 47 | 48 | func (s *Stream) String() string { 49 | return fmt.Sprintf("node: %s, label: %s, selector: %s", s.Node.String(), s.Label, s.Selector) 50 | } 51 | 52 | func (s *Stream) Get(index string) *Stream { 53 | if s.Selector != "" { 54 | panic(errors.New("stream already has a selector")) 55 | } 56 | return s.Node.Stream(s.Label, Selector(index)) 57 | } 58 | 59 | func (s *Stream) Audio() *Stream { 60 | return s.Get("a") 61 | } 62 | 63 | func (s *Stream) Video() *Stream { 64 | return s.Get("v") 65 | } 66 | 67 | func getStreamMap(streamSpec []*Stream) map[int]*Stream { 68 | m := map[int]*Stream{} 69 | for i := range streamSpec { 70 | m[i] = streamSpec[i] 71 | } 72 | return m 73 | } 74 | 75 | func getStreamMapNodes(streamMap map[int]*Stream) (ret []*Node) { 76 | for k := range streamMap { 77 | ret = append(ret, streamMap[k].Node) 78 | } 79 | return ret 80 | } 81 | 82 | func getStreamSpecNodes(streamSpec []*Stream) []*Node { 83 | return getStreamMapNodes(getStreamMap(streamSpec)) 84 | } 85 | 86 | type Node struct { 87 | streamSpec []*Stream 88 | name string 89 | incomingStreamTypes sets.String 90 | outgoingStreamType string 91 | minInputs int 92 | maxInputs int 93 | args []string 94 | kwargs KwArgs 95 | nodeType string 96 | } 97 | 98 | func NewNode(streamSpec []*Stream, 99 | name string, 100 | incomingStreamTypes sets.String, 101 | outgoingStreamType string, 102 | minInputs int, 103 | maxInputs int, 104 | args []string, 105 | kwargs KwArgs, 106 | nodeType string) *Node { 107 | n := &Node{ 108 | streamSpec: streamSpec, 109 | name: name, 110 | incomingStreamTypes: incomingStreamTypes, 111 | outgoingStreamType: outgoingStreamType, 112 | minInputs: minInputs, 113 | maxInputs: maxInputs, 114 | args: args, 115 | kwargs: kwargs, 116 | nodeType: nodeType, 117 | } 118 | n.__checkInputLen() 119 | n.__checkInputTypes() 120 | return n 121 | } 122 | 123 | func NewInputNode(name string, args []string, kwargs KwArgs) *Node { 124 | return NewNode(nil, 125 | name, 126 | nil, 127 | "FilterableStream", 128 | 0, 129 | 0, 130 | args, 131 | kwargs, 132 | "InputNode") 133 | } 134 | 135 | func NewFilterNode(name string, streamSpec []*Stream, maxInput int, args []string, kwargs KwArgs) *Node { 136 | return NewNode(streamSpec, 137 | name, 138 | sets.NewString("FilterableStream"), 139 | "FilterableStream", 140 | 1, 141 | maxInput, 142 | args, 143 | kwargs, 144 | "FilterNode") 145 | } 146 | 147 | func NewOutputNode(name string, streamSpec []*Stream, args []string, kwargs KwArgs) *Node { 148 | return NewNode(streamSpec, 149 | name, 150 | sets.NewString("FilterableStream"), 151 | "OutputStream", 152 | 1, 153 | -1, 154 | args, 155 | kwargs, 156 | "OutputNode") 157 | } 158 | 159 | func NewMergeOutputsNode(name string, streamSpec []*Stream) *Node { 160 | return NewNode(streamSpec, 161 | name, 162 | sets.NewString("OutputStream"), 163 | "OutputStream", 164 | 1, 165 | -1, 166 | nil, 167 | nil, 168 | "MergeOutputsNode") 169 | } 170 | 171 | func NewGlobalNode(name string, streamSpec []*Stream, args []string, kwargs KwArgs) *Node { 172 | return NewNode(streamSpec, 173 | name, 174 | sets.NewString("OutputStream"), 175 | "OutputStream", 176 | 1, 177 | 1, 178 | args, 179 | kwargs, 180 | "GlobalNode") 181 | } 182 | 183 | func (n *Node) __checkInputLen() { 184 | streamMap := getStreamMap(n.streamSpec) 185 | if n.minInputs >= 0 && len(streamMap) < n.minInputs { 186 | panic(fmt.Sprintf("Expected at least %d input stream(s); got %d", n.minInputs, len(streamMap))) 187 | } 188 | if n.maxInputs >= 0 && len(streamMap) > n.maxInputs { 189 | panic(fmt.Sprintf("Expected at most %d input stream(s); got %d", n.maxInputs, len(streamMap))) 190 | } 191 | } 192 | 193 | func (n *Node) __checkInputTypes() { 194 | streamMap := getStreamMap(n.streamSpec) 195 | for _, s := range streamMap { 196 | if !n.incomingStreamTypes.Has(s.Type) { 197 | panic(fmt.Sprintf("Expected incoming stream(s) to be of one of the following types: %s; got %s", n.incomingStreamTypes, s.Type)) 198 | } 199 | } 200 | } 201 | 202 | func (n *Node) __getIncomingEdgeMap() map[Label]NodeInfo { 203 | incomingEdgeMap := map[Label]NodeInfo{} 204 | streamMap := getStreamMap(n.streamSpec) 205 | for i, s := range streamMap { 206 | incomingEdgeMap[Label(fmt.Sprintf("%06v", i))] = NodeInfo{ 207 | Node: s.Node, 208 | Label: s.Label, 209 | Selector: s.Selector, 210 | } 211 | } 212 | return incomingEdgeMap 213 | } 214 | 215 | func (n *Node) Hash() int { 216 | b := 0 217 | for downStreamLabel, upStreamInfo := range n.IncomingEdgeMap() { 218 | b += getHash(fmt.Sprintf("%s%d%s%s", downStreamLabel, upStreamInfo.Node.Hash(), upStreamInfo.Label, upStreamInfo.Selector)) 219 | } 220 | b += getHash(n.args) 221 | b += getHash(n.kwargs) 222 | return b 223 | } 224 | 225 | func (n *Node) String() string { 226 | return fmt.Sprintf("%s (%s) <%s>", n.name, getString(n.args), getString(n.kwargs)) 227 | } 228 | 229 | func (n *Node) Equal(other DagNode) bool { 230 | return n.Hash() == other.Hash() 231 | } 232 | 233 | func (n *Node) ShortRepr() string { 234 | return n.name 235 | } 236 | 237 | func (n *Node) IncomingEdgeMap() map[Label]NodeInfo { 238 | return n.__getIncomingEdgeMap() 239 | } 240 | 241 | func (n *Node) GetInComingEdges() []DagEdge { 242 | return GetInComingEdges(n, n.IncomingEdgeMap()) 243 | } 244 | 245 | func (n *Node) Stream(label Label, selector Selector) *Stream { 246 | return NewStream(n, n.outgoingStreamType, label, selector) 247 | } 248 | 249 | func (n *Node) Get(a string) *Stream { 250 | l := strings.Split(a, ":") 251 | if len(l) == 2 { 252 | return n.Stream(Label(l[0]), Selector(l[1])) 253 | } 254 | return n.Stream(Label(a), "") 255 | } 256 | 257 | func (n *Node) GetFilter(outgoingEdges []DagEdge) string { 258 | if n.nodeType != "FilterNode" { 259 | panic("call GetFilter on non-FilterNode") 260 | } 261 | args, kwargs, ret := n.args, n.kwargs, "" 262 | if n.name == "split" || n.name == "asplit" { 263 | args = []string{fmt.Sprintf("%d", len(outgoingEdges))} 264 | } 265 | // args = Args(args).EscapeWith("\\'=:") 266 | for _, k := range kwargs.EscapeWith("\\'=:").SortedKeys() { 267 | v := getString(kwargs[k]) 268 | if v != "" { 269 | args = append(args, fmt.Sprintf("%s=%s", k, v)) 270 | } else { 271 | args = append(args, k) 272 | } 273 | } 274 | ret = escapeChars(n.name, "\\'=:") 275 | if len(args) > 0 { 276 | ret += fmt.Sprintf("=%s", strings.Join(args, ":")) 277 | } 278 | return escapeChars(ret, "\\'[],;") 279 | } 280 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "sort" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | func getInputArgs(node *Node) []string { 16 | var args []string 17 | if node.name == "input" { 18 | kwargs := node.kwargs.Copy() 19 | filename := kwargs.PopString("filename") 20 | format := kwargs.PopString("format") 21 | videoSize := kwargs.PopString("video_size") 22 | if format != "" { 23 | args = append(args, "-f", format) 24 | } 25 | if videoSize != "" { 26 | args = append(args, "-video_size", videoSize) 27 | } 28 | args = append(args, ConvertKwargsToCmdLineArgs(kwargs)...) 29 | args = append(args, "-i", filename) 30 | } else { 31 | panic("unsupported node input name") 32 | } 33 | return args 34 | } 35 | 36 | func formatInputStreamName(streamNameMap map[string]string, edge DagEdge, finalArg bool) string { 37 | prefix := streamNameMap[fmt.Sprintf("%d%s", edge.UpStreamNode.Hash(), edge.UpStreamLabel)] 38 | suffix := "" 39 | format := "[%s%s]" 40 | if edge.UpStreamSelector != "" { 41 | suffix = fmt.Sprintf(":%s", edge.UpStreamSelector) 42 | } 43 | if finalArg && edge.UpStreamNode.(*Node).nodeType == "InputNode" { 44 | format = "%s%s" 45 | } 46 | return fmt.Sprintf(format, prefix, suffix) 47 | } 48 | 49 | func formatOutStreamName(streamNameMap map[string]string, edge DagEdge) string { 50 | return fmt.Sprintf("[%s]", streamNameMap[fmt.Sprintf("%d%s", edge.UpStreamNode.Hash(), edge.UpStreamLabel)]) 51 | } 52 | 53 | func _getFilterSpec(node *Node, outOutingEdgeMap map[Label][]NodeInfo, streamNameMap map[string]string) string { 54 | var input, output []string 55 | for _, e := range node.GetInComingEdges() { 56 | input = append(input, formatInputStreamName(streamNameMap, e, false)) 57 | } 58 | outEdges := GetOutGoingEdges(node, outOutingEdgeMap) 59 | for _, e := range outEdges { 60 | output = append(output, formatOutStreamName(streamNameMap, e)) 61 | } 62 | return fmt.Sprintf("%s%s%s", strings.Join(input, ""), node.GetFilter(outEdges), strings.Join(output, "")) 63 | } 64 | 65 | func _getAllLabelsInSorted(m map[Label]NodeInfo) []Label { 66 | var r []Label 67 | for a := range m { 68 | r = append(r, a) 69 | } 70 | sort.Slice(r, func(i, j int) bool { 71 | return r[i] < r[j] 72 | }) 73 | return r 74 | } 75 | 76 | func _getAllLabelsSorted(m map[Label][]NodeInfo) []Label { 77 | var r []Label 78 | for a := range m { 79 | r = append(r, a) 80 | } 81 | sort.Slice(r, func(i, j int) bool { 82 | return r[i] < r[j] 83 | }) 84 | return r 85 | } 86 | 87 | func _allocateFilterStreamNames(nodes []*Node, outOutingEdgeMaps map[int]map[Label][]NodeInfo, streamNameMap map[string]string) { 88 | sc := 0 89 | for _, n := range nodes { 90 | om := outOutingEdgeMaps[n.Hash()] 91 | // todo sort 92 | for _, l := range _getAllLabelsSorted(om) { 93 | if len(om[l]) > 1 { 94 | panic(fmt.Sprintf(`encountered %s with multiple outgoing edges 95 | with same upstream label %s; a 'split'' filter is probably required`, n.name, l)) 96 | } 97 | streamNameMap[fmt.Sprintf("%d%s", n.Hash(), l)] = fmt.Sprintf("s%d", sc) 98 | sc += 1 99 | } 100 | } 101 | } 102 | 103 | func _getFilterArg(nodes []*Node, outOutingEdgeMaps map[int]map[Label][]NodeInfo, streamNameMap map[string]string) string { 104 | _allocateFilterStreamNames(nodes, outOutingEdgeMaps, streamNameMap) 105 | var filterSpec []string 106 | for _, n := range nodes { 107 | filterSpec = append(filterSpec, _getFilterSpec(n, outOutingEdgeMaps[n.Hash()], streamNameMap)) 108 | } 109 | return strings.Join(filterSpec, ";") 110 | } 111 | 112 | func _getGlobalArgs(node *Node) []string { 113 | return node.args 114 | } 115 | 116 | func _getOutputArgs(node *Node, streamNameMap map[string]string) []string { 117 | if node.name != "output" { 118 | panic("Unsupported output node") 119 | } 120 | var args []string 121 | if len(node.GetInComingEdges()) == 0 { 122 | panic("Output node has no mapped streams") 123 | } 124 | for _, e := range node.GetInComingEdges() { 125 | streamName := formatInputStreamName(streamNameMap, e, true) 126 | if streamName != "0" || len(node.GetInComingEdges()) > 1 { 127 | args = append(args, "-map", streamName) 128 | } 129 | } 130 | kwargs := node.kwargs.Copy() 131 | 132 | filename := kwargs.PopString("filename") 133 | if kwargs.HasKey("format") { 134 | args = append(args, "-f", kwargs.PopString("format")) 135 | } 136 | if kwargs.HasKey("video_bitrate") { 137 | args = append(args, "-b:v", kwargs.PopString("video_bitrate")) 138 | } 139 | if kwargs.HasKey("audio_bitrate") { 140 | args = append(args, "-b:a", kwargs.PopString("audio_bitrate")) 141 | } 142 | if kwargs.HasKey("video_size") { 143 | args = append(args, "-video_size", kwargs.PopString("video_size")) 144 | } 145 | 146 | args = append(args, ConvertKwargsToCmdLineArgs(kwargs)...) 147 | args = append(args, filename) 148 | return args 149 | } 150 | 151 | func (s *Stream) GetArgs() []string { 152 | var args []string 153 | nodes := getStreamSpecNodes([]*Stream{s}) 154 | var dagNodes []DagNode 155 | streamNameMap := map[string]string{} 156 | for i := range nodes { 157 | dagNodes = append(dagNodes, nodes[i]) 158 | } 159 | sorted, outGoingMap, err := TopSort(dagNodes) 160 | if err != nil { 161 | panic(err) 162 | } 163 | DebugNodes(sorted) 164 | DebugOutGoingMap(sorted, outGoingMap) 165 | var inputNodes, outputNodes, globalNodes, filterNodes []*Node 166 | for i := range sorted { 167 | n := sorted[i].(*Node) 168 | switch n.nodeType { 169 | case "InputNode": 170 | streamNameMap[fmt.Sprintf("%d", n.Hash())] = fmt.Sprintf("%d", len(inputNodes)) 171 | inputNodes = append(inputNodes, n) 172 | case "OutputNode": 173 | outputNodes = append(outputNodes, n) 174 | case "GlobalNode": 175 | globalNodes = append(globalNodes, n) 176 | case "FilterNode": 177 | filterNodes = append(filterNodes, n) 178 | } 179 | } 180 | // input args from inputNodes 181 | for _, n := range inputNodes { 182 | args = append(args, getInputArgs(n)...) 183 | } 184 | // filter args from filterNodes 185 | filterArgs := _getFilterArg(filterNodes, outGoingMap, streamNameMap) 186 | if filterArgs != "" { 187 | args = append(args, "-filter_complex", filterArgs) 188 | } 189 | // output args from outputNodes 190 | for _, n := range outputNodes { 191 | args = append(args, _getOutputArgs(n, streamNameMap)...) 192 | } 193 | // global args with outputNodes 194 | for _, n := range globalNodes { 195 | args = append(args, _getGlobalArgs(n)...) 196 | } 197 | if s.Context.Value("OverWriteOutput") != nil { 198 | args = append(args, "-y") 199 | } 200 | return args 201 | } 202 | 203 | func (s *Stream) WithTimeout(timeOut time.Duration) *Stream { 204 | if timeOut > 0 { 205 | s.Context, _ = context.WithTimeout(s.Context, timeOut) 206 | } 207 | return s 208 | } 209 | 210 | func (s *Stream) OverWriteOutput() *Stream { 211 | s.Context = context.WithValue(s.Context, "OverWriteOutput", struct{}{}) 212 | return s 213 | } 214 | 215 | func (s *Stream) WithInput(reader io.Reader) *Stream { 216 | s.Context = context.WithValue(s.Context, "Stdin", reader) 217 | return s 218 | } 219 | 220 | func (s *Stream) WithOutput(out ...io.Writer) *Stream { 221 | if len(out) > 0 { 222 | s.Context = context.WithValue(s.Context, "Stdout", out[0]) 223 | } 224 | if len(out) > 1 { 225 | s.Context = context.WithValue(s.Context, "Stderr", out[1]) 226 | } 227 | return s 228 | } 229 | 230 | func (s *Stream) WithErrorOutput(out io.Writer) *Stream { 231 | s.Context = context.WithValue(s.Context, "Stderr", out) 232 | return s 233 | } 234 | 235 | func (s *Stream) ErrorToStdOut() *Stream { 236 | return s.WithErrorOutput(os.Stdout) 237 | } 238 | 239 | type CommandOption func(cmd *exec.Cmd) 240 | 241 | var GlobalCommandOptions = make([]CommandOption, 0) 242 | 243 | type CompilationOption func(s *Stream, cmd *exec.Cmd) 244 | 245 | func (s *Stream) SetFfmpegPath(path string) *Stream { 246 | s.FfmpegPath = path 247 | return s 248 | } 249 | 250 | var LogCompiledCommand bool = true 251 | 252 | func (s *Stream) Silent(isSilent bool) *Stream { 253 | LogCompiledCommand = !isSilent 254 | return s 255 | } 256 | func (s *Stream) Compile(options ...CompilationOption) *exec.Cmd { 257 | args := s.GetArgs() 258 | cmd := exec.CommandContext(s.Context, s.FfmpegPath, args...) 259 | if a, ok := s.Context.Value("Stdin").(io.Reader); ok { 260 | cmd.Stdin = a 261 | } 262 | if a, ok := s.Context.Value("Stdout").(io.Writer); ok { 263 | cmd.Stdout = a 264 | } 265 | if a, ok := s.Context.Value("Stderr").(io.Writer); ok { 266 | cmd.Stderr = a 267 | } 268 | for _, option := range GlobalCommandOptions { 269 | option(cmd) 270 | } 271 | if LogCompiledCommand { 272 | log.Printf("compiled command: ffmpeg %s\n", strings.Join(args, " ")) 273 | } 274 | return cmd 275 | } 276 | 277 | func (s *Stream) Run(options ...CompilationOption) error { 278 | if s.Context.Value("run_hook") != nil { 279 | hook := s.Context.Value("run_hook").(*RunHook) 280 | go hook.f() 281 | defer func() { 282 | if hook.closer != nil { 283 | _ = hook.closer.Close() 284 | } 285 | <-hook.done 286 | }() 287 | } 288 | return s.Compile(options...).Run() 289 | } 290 | -------------------------------------------------------------------------------- /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 2017 Karl Kroening 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. -------------------------------------------------------------------------------- /ffmpeg_test.go: -------------------------------------------------------------------------------- 1 | package ffmpeg_go 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/u2takey/go-utils/rand" 10 | ) 11 | 12 | const ( 13 | TestInputFile1 = "./examples/sample_data/in1.mp4" 14 | TestOutputFile1 = "./examples/sample_data/out1.mp4" 15 | TestOverlayFile = "./examples/sample_data/overlay.png" 16 | ) 17 | 18 | func TestFluentEquality(t *testing.T) { 19 | base1 := Input("dummy1.mp4") 20 | base2 := Input("dummy1.mp4") 21 | base3 := Input("dummy2.mp4") 22 | t1 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 20}) 23 | t2 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 20}) 24 | t3 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 30}) 25 | t4 := base2.Trim(KwArgs{"start_frame": 10, "end_frame": 20}) 26 | t5 := base3.Trim(KwArgs{"start_frame": 10, "end_frame": 20}) 27 | 28 | assert.Equal(t, t1.Hash(), t2.Hash()) 29 | assert.Equal(t, t1.Hash(), t4.Hash()) 30 | assert.NotEqual(t, t1.Hash(), t3.Hash()) 31 | assert.NotEqual(t, t1.Hash(), t5.Hash()) 32 | } 33 | 34 | func TestFluentConcat(t *testing.T) { 35 | base1 := Input("dummy1.mp4", nil) 36 | trim1 := base1.Trim(KwArgs{"start_frame": 10, "end_frame": 20}) 37 | trim2 := base1.Trim(KwArgs{"start_frame": 30, "end_frame": 40}) 38 | trim3 := base1.Trim(KwArgs{"start_frame": 50, "end_frame": 60}) 39 | concat1 := Concat([]*Stream{trim1, trim2, trim3}) 40 | concat2 := Concat([]*Stream{trim1, trim2, trim3}) 41 | concat3 := Concat([]*Stream{trim1, trim3, trim2}) 42 | assert.Equal(t, concat1.Hash(), concat2.Hash()) 43 | assert.NotEqual(t, concat1.Hash(), concat3.Hash()) 44 | } 45 | 46 | func TestRepeatArgs(t *testing.T) { 47 | o := Input("dummy.mp4", nil).Output("dummy2.mp4", 48 | KwArgs{"streamid": []string{"0:0x101", "1:0x102"}}) 49 | assert.Equal(t, o.GetArgs(), []string{"-i", "dummy.mp4", "-streamid", "0:0x101", "-streamid", "1:0x102", "dummy2.mp4"}) 50 | } 51 | 52 | func TestGlobalArgs(t *testing.T) { 53 | o := Input("dummy.mp4", nil).Output("dummy2.mp4", nil).GlobalArgs("-progress", "someurl") 54 | 55 | assert.Equal(t, o.GetArgs(), []string{ 56 | "-i", 57 | "dummy.mp4", 58 | "dummy2.mp4", 59 | "-progress", 60 | "someurl", 61 | }) 62 | } 63 | 64 | func TestSimpleExample(t *testing.T) { 65 | err := Input(TestInputFile1, nil). 66 | Output(TestOutputFile1, nil). 67 | OverWriteOutput(). 68 | Run() 69 | assert.Nil(t, err) 70 | } 71 | 72 | func TestSimpleOverLayExample(t *testing.T) { 73 | err := Input(TestInputFile1, nil). 74 | Overlay(Input(TestOverlayFile), ""). 75 | Output(TestOutputFile1).OverWriteOutput(). 76 | Run() 77 | assert.Nil(t, err) 78 | } 79 | 80 | func TestSimpleOutputArgs(t *testing.T) { 81 | cmd := Input(TestInputFile1).Output("imageFromVideo_%d.jpg", KwArgs{"vf": "fps=3", "qscale:v": 2}) 82 | assert.Equal(t, []string{ 83 | "-i", "./examples/sample_data/in1.mp4", "-qscale:v", 84 | "2", "-vf", "fps=3", "imageFromVideo_%d.jpg"}, cmd.GetArgs()) 85 | } 86 | 87 | func TestAutomaticStreamSelection(t *testing.T) { 88 | // example from http://ffmpeg.org/ffmpeg-all.html 89 | input := []*Stream{Input("A.avi"), Input("B.mp4")} 90 | out1 := Output(input, "out1.mkv") 91 | out2 := Output(input, "out2.wav") 92 | out3 := Output(input, "out3.mov", KwArgs{"map": "1:a", "c:a": "copy"}) 93 | cmd := MergeOutputs(out1, out2, out3) 94 | printArgs(cmd.GetArgs()) 95 | printGraph(cmd) 96 | } 97 | 98 | func TestLabeledFiltergraph(t *testing.T) { 99 | // example from http://ffmpeg.org/ffmpeg-all.html 100 | in1, in2, in3 := Input("A.avi"), Input("B.mp4"), Input("C.mkv") 101 | in2Split := in2.Get("v").Hue(KwArgs{"s": 0}).Split() 102 | overlay := Filter([]*Stream{in1, in2}, "overlay", nil) 103 | aresample := Filter([]*Stream{in1, in2, in3}, "aresample", nil) 104 | out1 := Output([]*Stream{in2Split.Get("outv1"), overlay, aresample}, "out1.mp4", KwArgs{"an": ""}) 105 | out2 := Output([]*Stream{in1, in2, in3}, "out2.mkv") 106 | out3 := in2Split.Get("outv2").Output("out3.mkv", KwArgs{"map": "1:a:0"}) 107 | cmd := MergeOutputs(out1, out2, out3) 108 | printArgs(cmd.GetArgs()) 109 | printGraph(cmd) 110 | } 111 | 112 | func ComplexFilterExample() *Stream { 113 | split := Input(TestInputFile1).VFlip().Split() 114 | split0, split1 := split.Get("0"), split.Get("1") 115 | overlayFile := Input(TestOverlayFile).Crop(10, 10, 158, 112) 116 | return Concat([]*Stream{ 117 | split0.Trim(KwArgs{"start_frame": 10, "end_frame": 20}), 118 | split1.Trim(KwArgs{"start_frame": 30, "end_frame": 40})}). 119 | Overlay(overlayFile.HFlip(), ""). 120 | DrawBox(50, 50, 120, 120, "red", 5). 121 | Output(TestOutputFile1). 122 | OverWriteOutput() 123 | } 124 | 125 | func TestComplexFilterExample(t *testing.T) { 126 | assert.Equal(t, []string{ 127 | "-i", 128 | TestInputFile1, 129 | "-i", 130 | TestOverlayFile, 131 | "-filter_complex", 132 | "[0]vflip[s0];" + 133 | "[s0]split=2[s1][s2];" + 134 | "[s1]trim=end_frame=20:start_frame=10[s3];" + 135 | "[s2]trim=end_frame=40:start_frame=30[s4];" + 136 | "[s3][s4]concat=n=2[s5];" + 137 | "[1]crop=158:112:10:10[s6];" + 138 | "[s6]hflip[s7];" + 139 | "[s5][s7]overlay=eof_action=repeat[s8];" + 140 | "[s8]drawbox=50:50:120:120:red:t=5[s9]", 141 | "-map", 142 | "[s9]", 143 | TestOutputFile1, 144 | "-y", 145 | }, ComplexFilterExample().GetArgs()) 146 | } 147 | 148 | func TestCombinedOutput(t *testing.T) { 149 | i1 := Input(TestInputFile1) 150 | i2 := Input(TestOverlayFile) 151 | out := Output([]*Stream{i1, i2}, TestOutputFile1) 152 | assert.Equal(t, []string{ 153 | "-i", 154 | TestInputFile1, 155 | "-i", 156 | TestOverlayFile, 157 | "-map", 158 | "0", 159 | "-map", 160 | "1", 161 | TestOutputFile1, 162 | }, out.GetArgs()) 163 | } 164 | 165 | func TestFilterWithSelector(t *testing.T) { 166 | i := Input(TestInputFile1) 167 | 168 | v1 := i.Video().HFlip() 169 | a1 := i.Audio().Filter("aecho", Args{"0.8", "0.9", "1000", "0.3"}) 170 | 171 | out := Output([]*Stream{a1, v1}, TestOutputFile1) 172 | assert.Equal(t, []string{ 173 | "-i", 174 | TestInputFile1, 175 | "-filter_complex", 176 | "[0:a]aecho=0.8:0.9:1000:0.3[s0];[0:v]hflip[s1]", 177 | "-map", 178 | "[s0]", 179 | "-map", 180 | "[s1]", 181 | TestOutputFile1}, out.GetArgs()) 182 | 183 | } 184 | 185 | func ComplexFilterAsplitExample() *Stream { 186 | split := Input(TestInputFile1).VFlip().ASplit() 187 | split0 := split.Get("0") 188 | split1 := split.Get("1") 189 | 190 | return Concat([]*Stream{ 191 | split0.Filter("atrim", nil, KwArgs{"start": 10, "end": 20}), 192 | split1.Filter("atrim", nil, KwArgs{"start": 30, "end": 40}), 193 | }).Output(TestOutputFile1).OverWriteOutput() 194 | } 195 | 196 | func TestFilterConcatVideoOnly(t *testing.T) { 197 | in1 := Input("in1.mp4") 198 | in2 := Input("in2.mp4") 199 | args := Concat([]*Stream{in1, in2}).Output("out.mp4").GetArgs() 200 | assert.Equal(t, []string{ 201 | "-i", 202 | "in1.mp4", 203 | "-i", 204 | "in2.mp4", 205 | "-filter_complex", 206 | "[0][1]concat=n=2[s0]", 207 | "-map", 208 | "[s0]", 209 | "out.mp4", 210 | }, args) 211 | } 212 | 213 | func TestFilterConcatAudioOnly(t *testing.T) { 214 | in1 := Input("in1.mp4") 215 | in2 := Input("in2.mp4") 216 | args := Concat([]*Stream{in1, in2}, KwArgs{"v": 0, "a": 1}).Output("out.mp4").GetArgs() 217 | assert.Equal(t, []string{ 218 | "-i", 219 | "in1.mp4", 220 | "-i", 221 | "in2.mp4", 222 | "-filter_complex", 223 | "[0][1]concat=a=1:n=2:v=0[s0]", 224 | "-map", 225 | "[s0]", 226 | "out.mp4", 227 | }, args) 228 | } 229 | 230 | func TestFilterConcatAudioVideo(t *testing.T) { 231 | in1 := Input("in1.mp4") 232 | in2 := Input("in2.mp4") 233 | joined := Concat([]*Stream{in1.Video(), in1.Audio(), in2.HFlip(), in2.Get("a")}, KwArgs{"v": 1, "a": 1}).Node 234 | args := Output([]*Stream{joined.Get("0"), joined.Get("1")}, "out.mp4").GetArgs() 235 | assert.Equal(t, []string{ 236 | "-i", 237 | "in1.mp4", 238 | "-i", 239 | "in2.mp4", 240 | "-filter_complex", 241 | "[1]hflip[s0];[0:v][0:a][s0][1:a]concat=a=1:n=2:v=1[s1][s2]", 242 | "-map", 243 | "[s1]", 244 | "-map", 245 | "[s2]", 246 | "out.mp4", 247 | }, args) 248 | } 249 | 250 | func TestFilterASplit(t *testing.T) { 251 | out := ComplexFilterAsplitExample() 252 | args := out.GetArgs() 253 | assert.Equal(t, []string{ 254 | "-i", 255 | TestInputFile1, 256 | "-filter_complex", 257 | "[0]vflip[s0];[s0]asplit=2[s1][s2];[s1]atrim=end=20:start=10[s3];[s2]atrim=end=40:start=30[s4];[s3][s4]concat=n=2[s5]", 258 | "-map", 259 | "[s5]", 260 | TestOutputFile1, 261 | "-y", 262 | }, args) 263 | } 264 | 265 | func TestOutputBitrate(t *testing.T) { 266 | args := Input("in").Output("out", KwArgs{"video_bitrate": 1000, "audio_bitrate": 200}).GetArgs() 267 | assert.Equal(t, []string{"-i", "in", "-b:v", "1000", "-b:a", "200", "out"}, args) 268 | } 269 | 270 | func TestOutputVideoSize(t *testing.T) { 271 | args := Input("in").Output("out", KwArgs{"video_size": "320x240"}).GetArgs() 272 | assert.Equal(t, []string{"-i", "in", "-video_size", "320x240", "out"}, args) 273 | } 274 | 275 | func TestCompile(t *testing.T) { 276 | out := Input("dummy.mp4").Output("dummy2.mp4") 277 | assert.Equal(t, out.Compile().Args, []string{"ffmpeg", "-i", "dummy.mp4", "dummy2.mp4"}) 278 | } 279 | 280 | func TestPipe(t *testing.T) { 281 | 282 | width, height := 32, 32 283 | frameSize := width * height * 3 284 | frameCount, startFrame := 10, 2 285 | _, _ = frameCount, frameSize 286 | 287 | out := Input( 288 | "pipe:0", 289 | KwArgs{ 290 | "format": "rawvideo", 291 | "pixel_format": "rgb24", 292 | "video_size": fmt.Sprintf("%dx%d", width, height), 293 | "framerate": 10}). 294 | Trim(KwArgs{"start_frame": startFrame}). 295 | Output("pipe:1", KwArgs{"format": "rawvideo"}) 296 | 297 | args := out.GetArgs() 298 | assert.Equal(t, args, []string{ 299 | "-f", 300 | "rawvideo", 301 | "-video_size", 302 | fmt.Sprintf("%dx%d", width, height), 303 | "-framerate", 304 | "10", 305 | "-pixel_format", 306 | "rgb24", 307 | "-i", 308 | "pipe:0", 309 | "-filter_complex", 310 | "[0]trim=start_frame=2[s0]", 311 | "-map", 312 | "[s0]", 313 | "-f", 314 | "rawvideo", 315 | "pipe:1", 316 | }) 317 | 318 | inBuf := bytes.NewBuffer(nil) 319 | for i := 0; i < frameSize*frameCount; i++ { 320 | inBuf.WriteByte(byte(rand.IntnRange(0, 255))) 321 | } 322 | outBuf := bytes.NewBuffer(nil) 323 | err := out.WithInput(inBuf).WithOutput(outBuf).Run() 324 | assert.Nil(t, err) 325 | assert.Equal(t, outBuf.Len(), frameSize*(frameCount-startFrame)) 326 | } 327 | 328 | func TestView(t *testing.T) { 329 | a, err := ComplexFilterExample().View(ViewTypeFlowChart) 330 | assert.Nil(t, err) 331 | 332 | b, err := ComplexFilterAsplitExample().View(ViewTypeStateDiagram) 333 | assert.Nil(t, err) 334 | 335 | t.Log(a) 336 | t.Log(b) 337 | } 338 | 339 | func TestInputOrder(t *testing.T) { 340 | const streamNum = 12 341 | streams := make([]*Stream, 0, streamNum) 342 | expectedFilenames := make([]string, 0, streamNum) 343 | for i := 0; i < streamNum; i++ { 344 | filename := fmt.Sprintf("%02d.mp4", i) // These files don't exist, but it's fine in this test. 345 | expectedFilenames = append(expectedFilenames, filename) 346 | streams = append(streams, Input(filename)) 347 | } 348 | stream := Concat(streams).Output("output.mp4") 349 | args := stream.GetArgs() 350 | actualFilenames := make([]string, 0, streamNum) 351 | //Command looks like `-i 00.mp4 -i 01.mp4...` so actual filenames are the first streamNum even args 352 | for i := 1; i <= streamNum*2; i += 2 { 353 | actualFilenames = append(actualFilenames, args[i]) 354 | } 355 | assert.Equal(t, expectedFilenames, actualFilenames) 356 | } 357 | 358 | func printArgs(args []string) { 359 | for _, a := range args { 360 | fmt.Printf("%s ", a) 361 | } 362 | fmt.Println() 363 | } 364 | 365 | func printGraph(s *Stream) { 366 | fmt.Println() 367 | v, _ := s.View(ViewTypeFlowChart) 368 | fmt.Println(v) 369 | } 370 | 371 | //func TestAvFoundation(t *testing.T) { 372 | // out := Input("default:none", KwArgs{"f": "avfoundation", "framerate": "30"}). 373 | // Output("output.mp4", KwArgs{"format": "mp4"}). 374 | // OverWriteOutput() 375 | // assert.Equal(t, []string{"-f", "avfoundation", "-framerate", 376 | // "30", "-i", "default:none", "-f", "mp4", "output.mp4", "-y"}, out.GetArgs()) 377 | // err := out.Run() 378 | // assert.Nil(t, err) 379 | //} 380 | --------------------------------------------------------------------------------